Skip to content

Group metadata and app data

The app-data dictionary in the MLS group context carries the group’s identity, settings, role lists, membership, and component registry. This spec owns the component ids, snapshot and delta encodings, structural checks, and the metadata an app reads.

The dictionary’s bytes enter the MLS key schedule. Members that compute different bytes for one epoch cannot read each other’s messages.

In scope: component ids; snapshot and Update payload encodings; component structure and byte limits; creation of metadata; preservation of untouched bytes; write-once components; disappearing settings and expiry; version-floor encoding; and what an SDK exposes about metadata.

Out of scope: policy semantics and roles (PERM); proposal-list validity, commit validation, membership, and version-floor enforcement (GMOD); the commit-log signer and one-shot payload (FORK); DM identity and policy (DMS); consent (CONS); message content types (CTYPE); and archives (ARCH).

RelatedRelation
PERM-010, PERM-012, PERM-017, PERM-026Own hardcoded authority, pre-commit authorization, denial of unregistered writes, and action-policy validity.
GMOD-001, GMOD-005, GMOD-021Own permitted proposal types, membership content, and validation before commit application.
DMSReads CONVERSATION_TYPE and DM_MEMBERS from this spec and owns what a DM is.
JOIN-057, GMOD-022, GMOD-025, GMOD-026, GMOD-027Own Welcome holds, commit pauses, the monotonic floor, and version comparison.
FORK-010, FORK-050Own the signer key and the one-shot recovery payload.
EVENTEVENT-001 reports changed metadata and a group pause; EVENT-019 names changed metadata items.
TermMeaning
DictionaryThe app_data_dictionary group context extension (draft-ietf-mls-extensions §7.2.1): the map from component id to value bytes every member holds in its GroupContext.
ComponentOne entry of the dictionary: a component id and its value bytes.
Component idThe 16-bit key of a component. Its ranges are stated in section 1.
Well-known componentA component in the XMTP range whose id, type, and encoding section 2 fixes.
Application componentA component in the application range; registration and write authority follow PERM-010 and PERM-012.
RegistryCOMPONENT_REGISTRY: the map from component id to a ComponentMetadata entry that states the component’s type and write policies.
Hardcoded componentCOMPONENT_REGISTRY and SUPER_ADMIN_LIST: components whose write authority PERM-026 fixes in place of a registry entry.
Immutable componentA component whose id lies in an immutable sub-range: written once and never updated or removed.
Update, RemoveThe operations of an AppDataUpdate proposal (draft-ietf-mls-extensions-08 §4.7), distinct from MLS leaf-node Update and Remove proposals.
DeltaThe payload of an Update for a set or map component: an ordered list of mutations applied to the prior snapshot.
SnapshotThe full value the dictionary stores for a component.
Pre-commit dictionaryThe dictionary of the epoch before the commit being validated.
Version floorThe value of MIN_SUPPORTED_PROTOCOL_VERSION: the lowest client version that may process the group.
PausedThe state of a group whose version floor is greater than the client’s version.
Disappearing settingsThe timestamp MESSAGE_DISAPPEAR_FROM_NS and duration MESSAGE_DISAPPEAR_IN_NS, interpreted under META-050.
ExpiryThe deadline assigned under META-050; app visibility and deletion are governed separately by META-051 and META-063.
Application messageA stored message of the application kind: one an installation sent, as opposed to the record a client stores for a commit or a join.
Membership-change messageA message for a commit or join that carries a GroupUpdated payload.

The dictionary is the required representation for the metadata in this spec. Previously migrated groups can have this representation too. Active readers do not use legacy XMTP extensions as a fallback; legacy-only groups lack required dictionary state. A dictionary-bearing group can retain ignored legacy extensions. GMOD-001 owns rejection of GroupContextExtensions proposals, subject to the pre-commit floor pause in GMOD-025.

The dictionary and AppDataUpdate use draft-ietf-mls-extensions-08 §4.7. Their identifiers are registered in §7.2.1 and §7.3.1. XMTP assigns the component ids below.

RangeUse
0x0000–0x7FFFOutside the component space
0x8000–0xBDFFWell-known mutable components
0xBE00–0xBFFFWell-known immutable components
0xC000–0xFCFFApplication mutable components
0xFD00–0xFEFFApplication immutable components
0xFF00–0xFFFFReserved

Registry keys use the component id encoding in META-005. Every assigned XMTP or application id takes 4 bytes in that encoding.

GMOD-030 owns rejection of invalid proposal lists under the draft’s section 4.7, including mixed Update and Remove operations and repeated Removes for one component. META-064 governs successive XMTP payloads in a valid list. META-010 and META-013 own payload decoding and delta application.

IDTitleRequirementWhy
META-002Required capabilitiesWhen a client creates a group, it MUST list the extension type app_data_dictionary (0x0006) and the proposal type app_data_update (0x0008) in the group’s RequiredCapabilities extension (RFC 9420 §7.2).An installation that does not advertise them cannot read the dictionary or process a commit that updates it. Once added, it forks the group at its first update.
META-003Reserved ids are never usedWhen an Update or Remove targets a component id in the reserved range of section 1, the client MUST reject the commit.
META-004Immutable components are written onceWhen an immutable component is absent, the client MUST permit one initial scalar value or insert-only collection delta that passes authorization and structural validation. It MUST reject every Remove and every Update after that first value, including a later Update in the same commit.Changing a conversation type or DM pair invalidates the checks made at join time.
META-005Component id encodingWhen a client encodes a component id inside a component value, it MUST use the QUIC variable-length integer encoding of RFC 9000 §16, and MUST reject a value that decodes to more than 0xFFFF.

The well-known component table gives ids, type tags, snapshot encodings, and byte limits. Creation is governed by META-018. The snapshot is the value in the dictionary; it is not necessarily the Update payload.

Well-known user-scoped fields use the USER_ name prefix. Operator documentation recommends the same prefix for application field names, without making it a configuration validation rule. GROUP_IMAGE bytes are opaque to the client and can carry a remote attachment descriptor; the client never fetches or parses the remote object, so a failed download does not change group state. GROUP_IMAGE_URL and APP_DATA are unchanged.

Collection snapshots contain sorted, unique keys (META-012). Collection Updates carry ordered mutation sequences (META-013). Scalars carry the replacement value directly.

IdNamecomponent_type suffixSnapshot encoding
0x8000COMPONENT_REGISTRYTLS_MAP_BYTES_BYTESRegistryMap<ComponentId, ByteString>; values are serialized ComponentMetadata; bounded under META-068
0x8001SUPER_ADMIN_LISTTLS_SET_INBOX_IDTlsSet<InboxId>
0x8002ADMIN_LISTTLS_SET_INBOX_IDTlsSet<InboxId>
0x8003GROUP_MEMBERSHIPTLS_MAP_INBOX_ID_BYTESTlsMap<InboxId, ByteString>; values are serialized GroupMembershipEntry (GMOD-005)
0x8004GROUP_NAMESTRINGUTF-8, at most 100 bytes
0x8005GROUP_DESCRIPTIONSTRINGUTF-8, at most 1000 bytes
0x8006GROUP_IMAGE_URLSTRINGUTF-8, at most 2048 bytes
0x8007MESSAGE_DISAPPEAR_FROM_NSBYTES8-byte big-endian signed integer; nanoseconds since the Unix epoch
0x8008MESSAGE_DISAPPEAR_IN_NSBYTES8-byte big-endian signed integer; duration in nanoseconds
0x8009APP_DATASTRINGUTF-8, at most 8192 bytes
0x800AMIN_SUPPORTED_PROTOCOL_VERSIONSTRINGUTF-8 version under Semantic Versioning 2.0.0 §2
0x800BCOMMIT_LOG_SIGNERBYTESRaw signing-key bytes of FORK-010
0x800CUSER_DISPLAY_NAMETLS_MAP_INBOX_ID_STRINGTlsMap<InboxId, ByteString>; each value is valid UTF-8; bounded under META-068
0x800DGROUP_IMAGEBYTESOpaque bytes; bounded under META-068
0xBFFFCONVERSATION_TYPEBYTES4-byte big-endian value of a defined ConversationType other than CONVERSATION_TYPE_UNSPECIFIED
0xBFFECREATOR_INBOX_IDBYTESOne InboxId
0xBFFDDM_MEMBERSTLS_SET_INBOX_IDTlsSet<InboxId> with exactly two entries
0xBFFCONESHOT_MESSAGEBYTESSerialized OneshotMessage of FORK section 6

The collection wire block below uses TLS presentation language. K and T are type parameters, with the concrete substitutions in the type table below. <V> is the variable-length byte-count prefix of RFC 9420 §2.1.2. Each ByteString, including a byte-string key, carries its own prefix. Scalar payloads have no extra prefix inside ComponentData. TlsSet<K> is TlsMap<K, Empty>; an empty value has no bytes or length prefix.

opaque ByteString<V>;
struct {} Empty;
struct {
K key;
T value;
} TlsMapEntry;
struct {
TlsMapEntry entries<V>;
} TlsMap;
enum { insert(0), update(1), delete(2) } TlsMapMutationType; // one byte
struct {
TlsMapMutationType type;
K key;
select (type) {
case insert: T value;
case update: T value;
case delete: struct {};
};
} TlsMapMutation;
struct {
TlsMapMutation mutations<V>;
} TlsMapDelta;
enum { insert(0), remove(1), remove_by_hash(2) } TlsSetMutationType; // one byte
struct {
TlsSetMutationType type;
select (type) {
case insert: K key;
case remove: K key;
case remove_by_hash: opaque hash[32]; // SHA-256 of the TLS encoding of the key
};
} TlsSetMutation;
struct {
TlsSetMutation mutations<V>;
} TlsSetDelta;
struct {
varint version; // QUIC varint; 0, one byte
opaque id[32];
} InboxId;

The type table uses suffixes of the COMPONENT_TYPE_ enum values below. RegistryMap is the registry-specific TlsMap<ComponentId, ByteString> encoding. Despite its TLS_MAP_BYTES_BYTES tag, its keys are component ids under META-005, without a byte-string prefix. Its Update payload is TlsMapDelta<ComponentId, ByteString>.

Type suffixSnapshotUpdate payload
BYTESRaw bytesReplacement raw bytes
STRINGUTF-8 bytesReplacement UTF-8 bytes
TLS_MAP_BYTES_BYTESTlsMap<ByteString, ByteString>TlsMapDelta<ByteString, ByteString>
TLS_MAP_INBOX_ID_BYTESTlsMap<InboxId, ByteString>TlsMapDelta<InboxId, ByteString>
TLS_MAP_INBOX_ID_STRINGTlsMap<InboxId, ByteString>; each value must be valid UTF-8TlsMapDelta<InboxId, ByteString>; each inserted or updated value must be valid UTF-8
TLS_SET_BYTESTlsSet<ByteString>TlsSetDelta<ByteString>
TLS_SET_INBOX_IDTlsSet<InboxId>TlsSetDelta<InboxId>
// The data structure type of a component's value
enum ComponentType {
COMPONENT_TYPE_UNSPECIFIED = 0;
// Opaque bytes, replaced atomically
COMPONENT_TYPE_BYTES = 1;
// A utf-8 encoded string, replaced atomically
COMPONENT_TYPE_STRING = 2;
// A TlsMap<bytes, bytes> supporting key-level insert/update/delete via deltas
COMPONENT_TYPE_TLS_MAP_BYTES_BYTES = 3;
// A TlsMap<InboxId, bytes> supporting key-level insert/update/delete via deltas
COMPONENT_TYPE_TLS_MAP_INBOX_ID_BYTES = 4;
// A `TlsSet<bytes>` supporting insert/remove/remove-by-hash via deltas
COMPONENT_TYPE_TLS_SET_BYTES = 5;
// A `TlsSet<InboxId>` supporting insert/remove/remove-by-hash via deltas
COMPONENT_TYPE_TLS_SET_INBOX_ID = 6;
// A TlsMap<InboxId, UTF-8 bytes> with key-level insert/update/delete
COMPONENT_TYPE_TLS_MAP_INBOX_ID_STRING = 7;
}
message ComponentPermissions {
// Policy for inserting a new value (component does not yet exist)
MetadataPolicy insert_policy = 1;
// Policy for updating an existing value
MetadataPolicy update_policy = 2;
// Policy for deleting a value
MetadataPolicy delete_policy = 3;
}
message ComponentMetadata {
// The data structure type of the component's value
ComponentType component_type = 1;
// Permission policies for this component, evaluated against regular
// (member-issued) commits.
ComponentPermissions permissions = 2;
// Permission policies for this component, evaluated against MLS External
// Commits (RFC 9420 §12.4.3.2). Absent / unset is equivalent to all-Deny:
// external committers cannot touch this component.
ComponentPermissions external_committer_permissions = 3;
}

TLS_MAP_INBOX_ID_STRING uses the same TLS snapshot and delta encoding as TLS_MAP_INBOX_ID_BYTES. The type tag requires UTF-8 validation of every value in an Insert or Update delta and in the resulting snapshot; an invalid value rejects the commit.

MetadataPolicy is defined in PERM section 2. PERM-008 owns policy evaluation failure, and PERM-017 owns action-policy validity and the restricted admin policies. Structural decoding of an entry is separate from type dispatch and policy evaluation. An entry with an unknown or unspecified component_type can be structurally complete; it supplies no supported dispatch type. A present policy field can still contain an invalid policy tree.

The registry retains raw entry bytes, including entries that cannot be decoded or used. An authorized delta can repair or delete a mutable entry. Preservation applies to entries that the commit does not change (META-016), not to every entry in every later snapshot.

A backend catalogue definition is eligible for a conversation when its in_groups flag is true for a group, or its in_dms flag for a DM, and the client supports its component_type and every policy tag in its three member policies; no definition is eligible for a sync group. A newer backend can publish a tag an older client does not know. That client registers nothing for the ID rather than an entry it cannot write or validate, and a client that supports the tags registers it later under META-067.

Registration never forces a commit of its own. A membership refresh that finds nothing to change commits nothing, so a missing field waits for the next commit, of any kind, by a client with registry authority.

IDTitleRequirementWhy
META-010Encodings are bindingThe client MUST encode snapshots and Update payloads under the well-known component table, collection wire block, and type table above, including the registry-specific encoding. For an Update, it MUST decode the replacement scalar or collection delta, apply a delta to the preceding snapshot or an empty collection when absent, and reject the commit if decoding, application, or validation of the resulting snapshot and its stated byte limits fails. When a component has no built-in definition and its registry entry has an unknown or unspecified type, it MUST reject writes to that component.A payload and a snapshot have different encodings for collections.
META-011Inbox id encodingA client MUST encode an inbox id inside a component as the InboxId structure above, with a version of 0, and MUST reject a value whose version is not the single byte 0x00.
META-012Sorted collection snapshotsThe client MUST encode each TlsMap or TlsSet snapshot with unique keys in ascending order, and MUST reject a snapshot that is not in that form. It MUST compare component ids numerically and byte-string and inbox-id keys lexicographically by their value bytes.Different encodings of one set produce different group contexts.
META-013Deltas apply atomicallyThe client MUST apply delta mutations in their listed order, with each mutation using the preceding mutation’s result, and MUST NOT reject a delta merely for repeated or unsorted keys. It MUST resolve remove_by_hash against the snapshot before that delta, using SHA-256 of the key’s TLS encoding. If an insert names a present key, an update or deletion names an absent key, or a hash does not identify exactly one key, it MUST reject the commit without applying any of its changes.Partial application gives members different snapshots.
META-014Registry entry formWhen a registry delta names an id below the component space, reserved, or hardcoded, or inserts or updates bytes that do not decode as ComponentMetadata with permissions and all three member policy fields present, the client MUST reject the commit. It MUST also apply the policy-validity checks of PERM-017 to the resulting action policies.A missing policy cannot authorize a later write.
META-015Registry and immutable entries persistWhen an AppDataUpdate removes COMPONENT_REGISTRY, or a registry delta updates or deletes an immutable component’s entry, the client MUST reject the commit.Removing the registry prevents later authorization; changing an immutable entry can change interpretation of fixed bytes.
META-016Untouched registry entries are preservedWhen a commit does not explicitly change a registry entry, the client MUST preserve that entry’s bytes, including unknown protobuf fields and entries it cannot interpret.Re-encoding an untouched entry can remove bytes that other members retain.
META-017Untouched components pass throughWhen a client applies a commit, it MUST leave the bytes of every component the commit does not target unchanged, including a component it cannot decode.
META-018What a creator writesWhen a client creates a group, it MUST populate the epoch-0 dictionary with COMPONENT_REGISTRY, SUPER_ADMIN_LIST, ADMIN_LIST, GROUP_MEMBERSHIP, MIN_SUPPORTED_PROTOCOL_VERSION, CONVERSATION_TYPE, and CREATOR_INBOX_ID, with the creator set to its own inbox id. It MUST include each supplied name, description, image URL, app-data string, and disappearing setting, and omit each unsupplied optional value. It MUST initialize role lists under PERM-002 or DMS-002, the DM pair under DMS-002, and the signer and one-shot payload under FORK-010 and FORK-050.
META-066Register fields at creationWhen a client creates a group or DM, it MUST add to COMPONENT_REGISTRY each application definition in its configuration snapshot that is eligible for the conversation, using that definition’s component_type and permissions; it MUST add USER_DISPLAY_NAME and, for a group, GROUP_IMAGE with the core policies of PERM-029, and leave all their component values absent. A sync group MUST NOT receive those entries.A joiner reads field types and policies from the group, even when it has another backend snapshot.
META-067Register missing fieldsWhen a client with authority under PERM-026 builds a commit, it MUST include in that commit an Insert of each application definition eligible for the conversation whose ID has no raw key in the committed registry, subject to PERM-023; it MUST NOT overwrite a present raw entry with its backend copy.Replacing an unreadable entry from a local snapshot would make later writes depend on which client refreshed.
META-068Apply global field boundsWhen a client validates an application component, USER_DISPLAY_NAME, GROUP_IMAGE, or COMPONENT_REGISTRY, it MUST reject a scalar or collection key or value longer than 8192 bytes and a collection snapshot longer than 65536 bytes after serialization. A registry entry is measured as stored, unknown protobuf fields included.A registry type without a shared size bound lets a writer exceed the envelope budget and blocks later changes. A DM participant may write application-range registry entries, so an unbounded registry lets one participant grow every member’s group context.
META-019A joiner needs the identityWhen a Welcome’s group context has no dictionary, or its dictionary lacks COMPONENT_REGISTRY, CONVERSATION_TYPE, or CREATOR_INBOX_ID, or one of those does not decode under the table above, the client MUST record a terminal rejection for the Welcome (JOIN-048).Without them the joiner cannot tell which checks JOIN-060 demands of the group.

META-010 owns payload decoding and snapshot validation. PERM-010 fixes authorization to the pre-commit registry. These are separate inputs: sequential Updates use the preceding snapshot for delta application, while their authority still comes from the pre-commit state.

PERM-014 owns acceptance of components without built-in definitions. Their supported registry type selects the encoding in section 2. Component-specific invariants unknown to this client cannot be checked (Known limitations).

PERM-012 rejects writes without a usable registry entry, PERM-008 denies malformed policies, and META-010 rejects unsupported type dispatch. These rules do not prevent an authorized change to the registry entry itself. PERM-001 and PERM-026 own role evaluation and hardcoded write authority.

IDTitleRequirementWhy
META-064Successive Updates use successive snapshotsWhen a valid commit carries successive Updates for one component, the client MUST validate and apply each XMTP payload against the snapshot produced by the preceding Update, in the order defined by draft-ietf-mls-extensions-08 §4.7.A transition check against the original snapshot can miss a change made by an earlier Update.

Four components describe what a group is and never change: the conversation type, the creator, the DM pair, and the one-shot payload. They are immutable under META-004, so a joiner that validates them under JOIN-060 validates them for the life of the group.

// Defines the type of conversation
enum ConversationType {
CONVERSATION_TYPE_UNSPECIFIED = 0;
CONVERSATION_TYPE_GROUP = 1;
CONVERSATION_TYPE_DM = 2;
CONVERSATION_TYPE_SYNC = 3;
CONVERSATION_TYPE_ONESHOT = 4;
}

CONVERSATION_TYPE selects the checks required by JOIN-060. DMS-002 owns the DM pair at creation, PERM-002 owns non-DM role initialization, and PERM-004 owns the non-empty super-admin invariant.

IDTitleRequirementWhy
META-030The DM pair is exactly twoWhen DM_MEMBERS is present and does not decode to a set of exactly two InboxId values, or CONVERSATION_TYPE is not CONVERSATION_TYPE_DM, the client MUST reject the Welcome or commit that carries it.A pair of one or three is not a conversation between two people, and DMS derives the conversation’s identifier from exactly two. The pair grants DM-only authority, so a conversation of another type must not carry one.

The name, description, image URL, and app-data string are free text an app chooses; the client bounds them (META-010) and otherwise passes them through. A component that was never set is absent from the dictionary, and absent and empty are different bytes, so an app is given one answer for both: the empty string.

An app can guard an APP_DATA write with the value it read before it computed the new one. The guard is compared with the committed APP_DATA string of the group state the commit is prepared on, so a value another member committed in the meantime is detected and not overwritten. SEND-017 owns what a guard miss does to the intent.

A commit that changes a setting is recorded for the app as a membership-change message whose GroupUpdated payload lists each changed field by a stable name. The names below are what an app matches on, and an app written against them keeps working whatever the component id behind a name.

Componentfield_nameold_value and new_value form
GROUP_NAMEgroup_nameThe string
GROUP_DESCRIPTIONdescriptionThe string
GROUP_IMAGE_URLgroup_image_url_squareThe string
MESSAGE_DISAPPEAR_FROM_NSmessage_disappear_from_nsThe integer in decimal
MESSAGE_DISAPPEAR_IN_NSmessage_disappear_in_nsThe integer in decimal
MIN_SUPPORTED_PROTOCOL_VERSIONminimum_supported_protocol_versionThe string
COMMIT_LOG_SIGNER_commit_log_signerThe bytes in lowercase hex
APP_DATAapp_dataThe string
message GroupUpdated {
// Other fields and nested messages omitted.
// A summary of a change to the mutable metadata
message MetadataFieldChange {
// The field that was changed
string field_name = 1;
// The previous value
optional string old_value = 2;
// The updated value
optional string new_value = 3;
}
// The metadata changes in the commit
repeated MetadataFieldChange metadata_field_changes = 4;
}
IDTitleRequirementWhy
META-040Changes are recorded by nameWhen a commit changes the value of a component in the table above, the client MUST record in the membership-change message it stores for that commit a GroupUpdated.MetadataFieldChange as defined above, whose field_name and value form are the table’s, with old_value absent when the component was absent before, and MUST NOT record a change for a component whose value the commit left equal.An app reads the change log by field name, and a change recorded for an unchanged value shows the user an edit nobody made.
META-041Absent reads as emptyAn SDK MUST present an absent GROUP_NAME, GROUP_DESCRIPTION, GROUP_IMAGE_URL, or APP_DATA to an app as the empty string.
META-069Describe metadata fieldsWhen an app asks an SDK for a group’s metadata fields, the SDK MUST list every application-range registry entry that decodes as ComponentMetadata with all three member policies, plus each present GROUP_NAME, GROUP_DESCRIPTION, GROUP_IMAGE_URL, GROUP_IMAGE, APP_DATA, USER_DISPLAY_NAME, MESSAGE_DISAPPEAR_FROM_NS, and MESSAGE_DISAPPEAR_IN_NS entry. For each it MUST expose a field ref that holds the ID and, when available, a well-known name or a matching backend snapshot name; the committed member policies; and a component shape of Bytes, String, Map with key and value types, Set with key type, or Unknown with the raw type tag; it MUST derive the shape under META-010 for a well-known ID and from the registry type for an application ID; and it MUST set is_user_field to true exactly when the effective shape is a Map with an InboxID key. When an app asks for a field by name, the SDK MUST return the listed field with that name, preferring a well-known field over an application field, or none.Apps need type information for every developer-facing field, including one the local backend did not define, without handling raw IDs.
META-070Read component valuesWhen an app asks for a field listed under META-069, an SDK MUST support one field ref, a batch of field refs, and one key of a map component, resolving each ref by its component ID and reading the committed group dictionary. It MUST return a batch as a list of field ref and value records in request order. It MUST return STRING and TLS_MAP_INBOX_ID_STRING values as strings and byte-valued types as bytes.An app cannot render fields without reading the current group state or decoding the declared type.
META-071Write registered fieldsWhen an SDK writes a field listed under META-069, it MUST encode its operation under the META-010 type for a well-known ID or the committed registry type for an application ID, evaluate the committed registry policies, accept strings for STRING and TLS_MAP_INBOX_ID_STRING values and bytes for byte-valued types, and report a missing entry, unsupported type, type mismatch, or denied policy without using its backend snapshot as a substitute.A stale snapshot can describe another type or policy for the same ID.
META-072Read user dataWhen an app asks an SDK for user data with optional fields and inbox_ids filters, the SDK MUST select every is_user_field entry for fields = None or exactly the supplied field refs for fields = Some, rejecting a ref whose ID is absent or not a user field. It MUST select the current GROUP_MEMBERSHIP inboxes for inbox_ids = None in a group, the two DM_MEMBERS inboxes in a DM, or exactly the supplied inboxes for inbox_ids = Some, including former members. It MUST read one committed dictionary snapshot and return, for each selected inbox, a list of field ref and value records with only present selected values typed as Bytes or String; an inbox with no values has an empty list.An app can render a profile list without joining separate field maps or mixing group epochs.
META-073Write own user dataWhen an app asks an SDK to update its user data with values, a list of field refs with optional values, the SDK MUST infer its own inbox ID and reject a ref whose ID is absent, not a user field, repeated in values, or given a scalar type different from its map value type. It MUST encode each Some(value) as an Insert or Update of that inbox key and each None as a Delete when that key is present, then submit the deltas in one commit subject to the committed registry policies; an empty values list or a request that only clears absent keys makes no commit.Profile edits need a typed path that cannot accidentally name another inbox.
META-065A guard compares the committed app dataWhen an app supplies an expected value with an APP_DATA write, the client MUST compare it as a UTF-8 string with the APP_DATA value in the dictionary of the group state it prepares the commit on, and MUST NOT prepare the commit unless the two are equal. An absent APP_DATA component MUST NOT be equal to any expected value.A write built on a value another member has since replaced overwrites that member’s change, and neither app sees a conflict.

Disappearing settings define a start timestamp and a duration. META-050 uses the backend’s sent_at_ns as the start of each eligible message’s lifespan. META-051 excludes expired messages from app reads. META-063 governs later deletion from local storage.

The backend retains messages under its retention policy. A recipient can keep a copy outside the client. This feature does not erase copies already handed to an app.

IDTitleRequirementWhy
META-050Expiry from the sent timeWhen the client first stores a published application message, it MUST assign an expiry exactly when both disappearing settings are present and positive and sent_at_ns is not less than MESSAGE_DISAPPEAR_FROM_NS. It MUST compute that expiry as sent_at_ns plus MESSAGE_DISAPPEAR_IN_NS using checked signed 64-bit addition, with a result above 9223372036854775807 clamped to that value. It MUST retain that expiry through later setting changes and MUST NOT assign a disappearing expiry to a membership-change message.Processing-time expiry gives an offline installation a longer retention period.
META-051Expired messages are hiddenWhen a message’s expiry is not later than the current time, the client MUST exclude it from history queries, direct message-id lookups, and new stream deliveries to an app.A delayed delete must not extend the message’s visible life.
META-063Delete at the cleanup passWhen a local disappearing-message cleanup pass completes successfully, the client MUST have deleted every published application message whose expiry was not later than the time that pass began.A sender relies on cooperating recipients removing expired local copies.
META-052Settings are readable and settableAn SDK MUST let an app read a conversation’s disappearing settings, set both values, and clear them. It MUST report them as enabled exactly when both values are present and greater than 0.

A group can require a minimum client version. Section 2 owns the component’s encoding. GMOD-026 owns monotonic changes, GMOD-027 owns version comparison and malformed-floor handling, and JOIN-057 owns the Welcome hold.

GMOD-022 requires every other validation check to pass before a floor-setting commit can pause the group. Authorization alone is insufficient. GMOD-025 owns the publication stop and resumption point; its hold follows PROC-012.

PERM-023 owns the prior floor bump for changed acceptance rules, including new registry-entry formats, component types, and delta mutations.

IDTitleRequirementWhy
META-062Apps see the pauseAn SDK MUST let an app read whether a group is paused and the floor it is paused for.

A client cannot apply a component whose type or entry format it cannot interpret. Without the preceding floor bump described in section 7, a commit using that form can be accepted by newer clients and rejected by older clients.

A component without a built-in definition is applied by its supported registry type (PERM-014 and META-010). An older client cannot check a component-specific bound or transition rule introduced by a later release.

Every rejection of an AppDataUpdate proposal, whether for authority, for a malformed payload, or for an invariant, is reported to the sender as one error kind. An app cannot tell a permission failure from a malformed value.

Cleanup depends on local execution; META-063 imposes no wall-clock deadline while the client or cleanup work is stopped. Apps can retain copies already delivered to them.

Legacy-only groups lack the dictionary identity required by META-019. This spec defines no migration procedure. Previously migrated groups and dictionary-bearing groups with ignored legacy extensions are not excluded.