Group Creation and Messaging
Group Creation Flow
Creating a group involves a server-side operation followed by MLS group initialization.
sequenceDiagram
participant C as Creator
participant S as Server
C->>S: POST /api/v1/groups<br>CreateGroupRequest { group_name, alias? }
Note right of S: Create group record<br>Add creator as admin
S-->>C: 201 CreateGroupResponse { group_id }
Note over C: MLS: create_group()<br>MLS: commit_builder().build()<br>MLS: apply_pending_commit()<br>→ commit, group_info, mls_group_id
C->>S: POST /groups/{id}/commit<br>UploadCommitRequest { commit_message, group_info, mls_group_id }
Note right of S: Store commit as msg seq 1<br>Store GroupInfo<br>Set mls_group_id
S-->>C: 200 OK
Note over C: Store mapping: group_id → mls_group_id
Steps
-
Create on server: The client sends the group name and optional alias. The server creates the group record and adds the creator as the sole member with the
adminrole. -
Initialize MLS group: The client creates an MLS group locally. The initial MLS group has only the creator. The client builds an initial commit (which establishes the group’s cryptographic state) and extracts the GroupInfo.
-
Upload commit: The client uploads the commit, GroupInfo, and the hex-encoded MLS group ID. The commit is stored as the first message (sequence number 1). The
mls_group_idis recorded in the group’s server record. -
Store mapping: The client stores the mapping from the server’s
group_idto the MLSmls_group_idfor future operations.
Sending a Message
sequenceDiagram
participant Sn as Sender
participant S as Server
participant R as Recipient
Note over Sn: MLS: encrypt_application_message(plaintext)<br>→ ciphertext
Sn->>S: POST /groups/{id}/messages<br>SendMessageRequest { mls_message: ciphertext }
Note right of S: Store ciphertext blob<br>Assign sequence_num
S-->>Sn: 200 SendMessageResponse { sequence_num }
S--)R: SSE: NewMessageEvent<br>{ group_id, seq, sender_id }
Steps
-
Encrypt: The client encrypts the plaintext message using MLS, producing an opaque ciphertext blob.
-
Send: The client sends the ciphertext to the server. The server stores it as an opaque blob and assigns a monotonically increasing sequence number.
-
Notify: The server broadcasts a
NewMessageEventto all group members except the sender, indicating that a new message is available.
Receiving Messages
sequenceDiagram
participant S as Server
participant R as Recipient
S--)R: SSE: NewMessageEvent<br>{ group_id, seq, sender_id }
R->>S: GET /groups/{id}/messages?after={last_seen_seq}
S-->>R: 200 GetMessagesResponse<br>{ messages: [{ seq, sender_id, mls_message, created_at }] }
Note over R: MLS: process_incoming_message<br>→ plaintext<br>Resolve sender_id → display name<br>Display message
Steps
-
Receive notification: The client receives a
NewMessageEventvia SSE, indicating a new message is available in a group. -
Fetch messages: The client fetches messages with sequence numbers after its last known sequence number using the
afterquery parameter. -
Decrypt: For each message, the client processes it through the MLS layer:
- Application messages produce decrypted plaintext (chat messages).
- Commit messages produce roster change information (members added/removed, key rotations).
- Failed decryption produces an error reason (epoch evicted, key missing, etc.).
-
Resolve sender: The client maps the
sender_idto a display name using its local member cache or the user lookup endpoint. -
Update tracking: The client updates its last-seen sequence number to avoid re-fetching processed messages.
Commit Messages vs. Application Messages
Both commits and application messages are stored in the same message table with sequential sequence numbers. The client distinguishes between them during MLS decryption:
- Application messages:
process_incoming_message()returns decrypted plaintext bytes. These are user-visible chat messages. - Commit messages:
process_incoming_message()returns a commit result with information about roster changes (members added, members removed). Clients typically display these as system messages (e.g., “Alice joined the group”, “Group keys updated”).
An empty commit (no proposals, just epoch advancement) indicates a key rotation for forward secrecy.