Skip to content

Groups

Robust group chat permissions are key to providing users with a friendly and safe group chat experience.

Member statuses are the roles that can be assigned to each participant (inbox ID) in a group chat. These are the available member statuses:

  • Member
    • Everyone in a group chat is a member. A member can be granted admin or super admin status. If a member’s admin or super admin status is removed, they are still a member of the group.
  • Admin
  • Super admin

When a group is created, all groups have the same initial member “roles” set:

  • There is one super admin, and it is the group creator
  • There are no admins
  • Each user added to the group starts out as a member

A super admin can change the group’s permission policies. Each action uses the policy assigned to it.

PermissionAllow allDeny allAdmin onlySuper admin only
Add member✅✅✅✅
Remove member✅✅✅✅
Add admin❌✅✅✅
Remove admin❌✅✅✅
Update group permissions❌❌❌✅
Update group metadata✅✅✅✅

If you do not select a policy set, the group uses All members.

PermissionAll membersAdmin only
Add memberAll membersAdmin
Remove memberAdminAdmin
Add or remove adminSuper adminSuper admin
Update permissionsSuper adminSuper admin
Update name, description, image, app dataAll membersAdmin
Update disappearing messagesAdminAdmin

DMs use neither set. Participants cannot change membership or permissions, and both can change metadata.

Add admin and remove admin cannot use an allow-all policy. A custom policy can allow admins to perform these actions. Only a super admin can update the permission policies or grant and revoke the super admin role.

A custom set contains nine policies: add member, remove member, add admin, remove admin, update name, update description, update image URL, update disappearing messages, and update app data.

Pass a custom GroupPermissionMode in CreateGroupOptions.permissions. The custom variant contains a PermissionPolicySet. This option is available on all four SDKs.

All four SDKs use group.updatePermission(kind, policy, metadataField). Use PermissionUpdateKind for the action and PermissionPolicy for the policy. For a metadata update, also pass MetadataFieldKind. This includes the disappearing-message and app-data policies. Swift uses the labels kind:, policy:, and metadataField:.

ActionMethod
Check roleisAdmin(inboxId), isSuperAdmin(inboxId)
List roleslistAdmins(), listSuperAdmins()
Grant roleaddAdmin(inboxId), addSuperAdmin(inboxId)
Revoke roleremoveAdmin(inboxId), removeSuperAdmin(inboxId)

These names are the same on all four SDKs. The calls are asynchronous. Swift uses inboxId: for the methods that take an inbox ID.

The backend sets the group member limit. The default limit is 250 inboxes.

Use addMembers and removeMembers with a list of inbox IDs or a list of PublicIdentity values. Swift uses the members: label. Call members() to read the member list on all four SDKs. These calls are asynchronous.

A PublicIdentity list can hold handles and accounts together, and each call makes one commit. A handle that no inbox claimed fails the call with IdentityNotFound before any change.

Sync before you read the member list.

Each Member has handle, the inbox’s current handle, and otherHandles, its other handles with the most recent first. Show handle as the text that identifies the member, and show otherHandles only as secondary information. See Show handles.

A member can request removal from a group by calling requestRemoval(). A super admin must process the request before the member is removed. After removal, the group becomes inactive for that member.

The call is refused if the caller is a super admin, is the only member, or is in a DM. After the request is processed locally, group.state() reports membershipState as pending removal. This does not mean that the member has already been removed.

Group chats can have metadata, like names, descriptions, and images. Metadata can help users more easily identify their group chats.

FieldUpdateLimit
NameupdateName(name)100 bytes
DescriptionupdateDescription(description)1,000 bytes
Image URLupdateImageUrl(url)2,048 bytes
App dataupdateAppData(data, expected)8,192 bytes

Read metadata from the asynchronous group.state() snapshot on all four SDKs. App data is a string. Pass the previous value as expected to update app data only if it still matches, or pass undefined on Browser and Node or null/nil on mobile to omit that check. Serialize structured data before you set it. See Disappearing messages.

An operator can publish additional fields through [[application_components]]. These fields can hold a group value, a set, or a map. An inbox-keyed map holds one value per user. All members who can read the conversation can read these values. User fields are conversation data, not a global inbox profile.

New groups and DMs register the fields selected by in_groups and in_dms. Registration defines the type and the insert, update, and delete policies. It does not set a value. Existing conversations can register missing fields when a client with registry write authority makes a commit. A backend configuration change does not replace a field already in the conversation’s registry.

Use the conversation’s descriptors to build a form or validate a value:

MethodResult
metadataFields()Registered field descriptors, with refs, types, policies, and isUserField.
metadataField(name)The descriptor with this name, or no result.
metadataValue(field)A scalar, map, or set value, or no value if unset.
metadataValues(fields)Field and value records in the order requested.
mapValue(field, key)One map entry, or no value if the key is absent.
updateMetadataField(field, operation)A typed change checked against the committed field policies.

A MetadataFieldRef identifies a field by componentId. Its name is a label from the well-known fields or the client’s backend configuration. A field can have no name when the local configuration does not name its ID. Keep the ref from the descriptor. Do not use a backend definition to assume the type or policy of an existing field.

Use ComponentMutation.Replace on Kotlin or .replace on Swift for a scalar. Use MapDelta or SetDelta on Kotlin, or .mapDelta or .setDelta on Swift, for collection changes. Map operations insert, update, or delete one key; set operations insert or delete one key. A field in the immutable application range can be set only once. Wrong types, unknown fields, unsupported types, and denied writes produce typed errors.

Browser and Node use records with kind: "replace", kind: "mapDelta", or kind: "setDelta". A scalar value is a FieldValue with kind: "string" or kind: "bytes" and a matching value.

Application scalars, collection keys, and collection values have an 8,192-byte limit. A serialized collection has a 65,536-byte limit. These limits also apply to USER_DISPLAY_NAME and GROUP_IMAGE. The existing string fields retain the limits in the table above.

USER_DISPLAY_NAME is a well-known string map keyed by inbox ID. It is registered in new groups and DMs. Members can set or remove their own value. The default policy also lets members remove an entry for an inbox that has left. An application field such as USER_PRONOUNS can use the same shape and policies. The USER_ prefix is a naming convention. The inbox-keyed map type determines whether isUserField is true.

updateUserData(values) uses the caller’s inbox ID. Each value sets that inbox’s entry; an undefined value on Browser and Node, null on Kotlin, or nil on Swift removes it. All changes in the call use one commit. Duplicate fields, non-user fields, and type mismatches fail the call. Clearing an absent entry makes no change.

The following examples run with an existing group. The operator must publish GROUP_CATEGORY and USER_PRONOUNS as shown in the backend guide. The group must have registered them, and the caller must be an admin to set the category.

const
const category: MetadataFieldDescriptor | undefined
category
= await
group: Group
group
.
Group.metadataField(name: string): Promise<MetadataFieldDescriptor | undefined>
metadataField
("GROUP_CATEGORY");
const
const displayName: MetadataFieldDescriptor | undefined
displayName
= await
group: Group
group
.
Group.metadataField(name: string): Promise<MetadataFieldDescriptor | undefined>
metadataField
("USER_DISPLAY_NAME");
const
const pronouns: MetadataFieldDescriptor | undefined
pronouns
= await
group: Group
group
.
Group.metadataField(name: string): Promise<MetadataFieldDescriptor | undefined>
metadataField
("USER_PRONOUNS");
if (!
const category: MetadataFieldDescriptor | undefined
category
|| !
const displayName: MetadataFieldDescriptor | undefined
displayName
|| !
const pronouns: MetadataFieldDescriptor | undefined
pronouns
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("The group has not registered the required fields");
}
await
group: Group
group
.
Group.updateMetadataField(field: MetadataFieldRef, operation: ComponentMutation): Promise<void>
updateMetadataField
(
const category: MetadataFieldDescriptor
category
.
field: MetadataFieldRef
field
, {
kind: "replace"
kind
: "replace",
value: FieldValue
value
: {
kind: "string"
kind
: "string",
value: string
value
: "engineering" },
});
await
group: Group
group
.
Group.updateUserData(values: Array<UserFieldUpdate>): Promise<void>
updateUserData
([
{
field: MetadataFieldRef
field
:
const displayName: MetadataFieldDescriptor
displayName
.
field: MetadataFieldRef
field
,
value?: FieldValue | undefined
value
: {
kind: "string"
kind
: "string",
value: string
value
: "Alix" } },
{
field: MetadataFieldRef
field
:
const pronouns: MetadataFieldDescriptor
pronouns
.
field: MetadataFieldRef
field
,
value?: FieldValue | undefined
value
: {
kind: "string"
kind
: "string",
value: string
value
: "they/them" } },
]);
await
group: Group
group
.
Group.sync(): Promise<void>
sync
();
const
const profiles: Map<string, UserFieldValue[]>
profiles
= await
group: Group
group
.
Group.userData(fields: Array<MetadataFieldRef> | undefined, inboxIds: Array<InboxId> | undefined): Promise<Map<InboxId, Array<UserFieldValue>>>
userData
(
[
const displayName: MetadataFieldDescriptor
displayName
.
field: MetadataFieldRef
field
,
const pronouns: MetadataFieldDescriptor
pronouns
.
field: MetadataFieldRef
field
],
var undefined
undefined
,
);
await
group: Group
group
.
Group.updateUserData(values: Array<UserFieldUpdate>): Promise<void>
updateUserData
([{
field: MetadataFieldRef
field
:
const pronouns: MetadataFieldDescriptor
pronouns
.
field: MetadataFieldRef
field
,
value?: FieldValue | undefined
value
:
var undefined
undefined
}]);

userData(fields, inboxIds) returns a map from inbox ID to present field and value records. Omit the filters with undefined on Browser and Node, null on Kotlin, or nil on Swift to read all user fields for current members or the two DM participants. An empty filter selects nothing. An inbox with no selected values has an empty list. Supply inbox IDs to read entries for former members. Sync before reads when you need changes from other members.

GROUP_IMAGE holds opaque bytes. It is separate from the GROUP_IMAGE_URL string and is registered in new groups. An app can upload an image with the attachment API, serialize the returned RemoteAttachment, and store those bytes with updateMetadataField. Agree on the serialization format with the other apps that read the group. The SDK does not parse or fetch these bytes. A reader decodes the descriptor and explicitly requests the attachment download.