Revision 1, 2026-04-28
SimpleX Channels Protocol
For architecture, design rationale, security properties, and threat model, see SimpleX Channels Overview.
Table of contents
Protocol
This document describes the channel protocol as currently implemented. It builds on the SimpleX Chat Protocol, using the same message format and connection model, with extensions for relay-mediated distribution and cryptographic message signing.
Channel creation
Creating a channel involves generating cryptographic material, creating the channel link, and connecting relay members:
-
Key generation. The owner generates an Ed25519 root key pair. The entity ID is computed as
sha256(rootPubKey). A separate member key pair is generated for message signing, and anOwnerAuthentry is created, signed by the root key. -
Link creation. The owner calls the agent's
prepareConnectionLinkAPI with the root key pair and entity ID. This returns a prepared link (including aConnShortLinkaddress) without any network calls. The link address is deterministic, derived from the fixed data hash, so it can be embedded in the group profile immediately. -
Link data upload. The owner calls
createConnectionForLink, which makes a single network call to create the SMP queue and upload the encrypted link data. The link's fixed data contains the root public key and connection request. The mutable user data contains theOwnerAutharray, the channel profile (including the entity ID and the link itself), and the initial subscriber count. -
Relay invitation. For each selected relay, the owner sends a contact request containing an
x.grp.relay.invmessage with the channel's short link. The relay retrieves the link data, validates the channel profile, creates its own relay link (with the channel's entity ID in its immutable data), and responds withx.grp.relay.acptcontaining its relay link. -
Link update. As each relay accepts and provides its relay link, the owner validates that the relay link contains the correct entity ID, then adds the relay link to the channel link's mutable data.
-
Local record. The channel is stored on the owner's device with the root private key, member private key, and channel profile. This local record is the authoritative state of the channel.
Relay acceptance
When a relay receives an invitation to serve a channel, it validates the channel and creates its own relay link. This flow is currently part of channel creation; adding relays to an existing channel is planned but not yet implemented.
-
Owner sends
x.grp.relay.invto the relay's contact address. This message includes the relay's member ID and role, the owner's profile, and the channel's short link. -
Relay receives the invitation and creates a relay request record. A relay request worker processes it asynchronously.
-
The worker retrieves the channel's link data from the SMP server, extracts and validates the channel profile and owner authorization.
-
The relay creates its own contact address link (the relay link) with the channel's entity ID in the immutable fixed data.
-
The relay accepts the owner's connection request, sending its relay link in the acceptance.
-
The owner retrieves the relay link data, validates that the entity ID in the relay link matches the channel's entity ID, and adds the relay link to the channel link's user data.
TODO: Periodic monitoring where the relay retrieves channel link data to verify its relay link is still listed is planned but not yet implemented.
Subscriber connection
A subscriber joins a channel through the following flow:
-
Link retrieval. The subscriber scans or receives the channel's short link. The client retrieves the link data, which contains the channel profile, owner authorization chain, and list of relay links.
-
Relay link resolution. For each relay link listed, the client resolves the
ConnectionRequestUrifrom the relay's short link. -
Connection. The client connects to relays - the first synchronously, the rest asynchronously. Each connection sends an
x.membermessage with the subscriber's profile (or an incognito profile, created once and shared with all relays), member ID, and member signing key. -
Relay acceptance. Each relay accepts the connection, creates a member record for the subscriber with the configured subscriber role (default
observer), and sends anx.grp.link.invmessage with the channel profile and group link invitation data. -
Introduction. The relay introduces the new subscriber to the channel's moderators and owners by sending an
x.grp.mem.newmessage. It also sends moderator/owner profiles to the subscriber. -
History. If the channel has history sharing enabled, the relay sends recent cached history to the new subscriber.
The subscriber is functional (can receive messages) as soon as at least one relay connection succeeds. Additional relay connections provide redundancy and cross-relay consistency checking.
Message signing
Messages that alter the channel's roster, profile, or administrative state are cryptographically signed by the sending owner. Content messages are not signed by default; see Signing scope for the rationale.
Which messages require signatures:
| Message | Description | Signed |
|---|---|---|
x.grp.del |
Delete channel | Required |
x.grp.info |
Update channel profile | Required |
x.grp.prefs |
Update channel preferences | Required |
x.grp.mem.del |
Remove member | Required |
x.grp.mem.role |
Change member role | Required |
x.grp.mem.restrict |
Restrict member | Required |
x.grp.leave |
Leave channel | Required (unverified allowed between subscribers) |
x.info |
Update member profile | Required (unverified allowed between subscribers) |
x.msg.new |
Content message | Not signed |
x.msg.update |
Edit message | Not signed |
x.msg.del |
Delete message | Not signed |
Signing process:
The signing context binds the signature to a specific channel and sender:
bindingPrefix = smpEncode(CBGroup) <> smpEncode(publicGroupId, memberId)
signedBytes = bindingPrefix <> messageBody
signature = Ed25519.sign(memberPrivKey, signedBytes)
The binding prefix includes the chat binding tag ("G" for group), the channel's entity ID, and the sender's member ID. This prevents cross-channel and cross-member replay attacks - a signature valid in one channel cannot be reused in another.
Verification process:
When a subscriber receives a signed message:
-
The signature is present: reconstruct the binding prefix from the channel's stored entity ID and the sender's member ID. Verify all signatures against the sender's stored public key. If all verify, the message is accepted as verified.
-
The signature is present but the sender's key is unknown: the message is accepted as signed-but-unverified only if the event does not require a signature. For
x.grp.leaveandx.infobetween subscribers whose keys haven't been exchanged yet, unverified signatures are permitted as a temporary measure. -
No signature is present: the message is accepted only if the event does not require a signature (i.e., the channel does not use relays, or the event is a content message).
If verification fails for a message that requires a signature, the message is rejected and a bad signature event is shown to the user.
Message forwarding
Content originates on the owner's device and flows through relays to subscribers. The forwarding mechanism preserves the original message bytes, including any signature, without re-encoding:
Owner to Relay: The owner sends messages directly to each relay over their SMP connection. Messages are encoded in binary batch format.
Relay processing: When a relay receives a message from an owner, it:
- Parses and processes the message locally (updating its cached state, e.g. for roster changes).
- If the relay is configured to forward for this channel, creates a delivery task for each message that should be forwarded to subscribers. The task records the message ID, the sender's member ID, the broker timestamp, and whether the message was sent as the channel (not attributed to a specific owner).
- The delivery task is persisted to the database for delivery reliability - ensuring forwarding can resume after a relay crash.
Relay to Subscribers: A delivery task worker reads pending tasks, batches them into delivery jobs, and a delivery job worker sends each job to subscribers in paginated batches (using a cursor over group member IDs).
For forwarded messages from subscribers to owners (e.g. support scope messages), the relay wraps the message in a forwarding envelope:
forwardEnvelope = ">" <> smpEncode(GrpMsgForward) <> encodeBatchElement(signedMsg, msgBody)
This preserves the original message's signature bytes verbatim.
Binary batch format
Channels use a binary batch format that preserves exact message bytes for signature verification. This is distinct from the JSON array batching used by regular groups.
binaryBatch = %s"=" elementCount *batchElement
elementCount = 1*1 OCTET ; 1-255 elements
batchElement = elementLen elementBody
elementLen = 2*2 OCTET ; 16-bit big-endian length
elementBody = signedElement / forwardElement / plainElement / fileElement
signedElement = %s"/" chatBinding sigCount *msgSignature jsonBody
forwardElement = %s">" grpMsgForward (signedElement / plainElement)
plainElement = %s"{" *OCTET ; JSON message body
fileElement = %s"F" *OCTET ; binary file chunk
chatBinding = 1*1 OCTET ; "G" (group), "D" (direct), "C" (channel)
sigCount = 1*1 OCTET ; number of signatures (1-255)
msgSignature = keyRef sigBytes
keyRef = %s"M" ; member key reference
sigBytes = 64*64 OCTET ; Ed25519 signature
grpMsgForward = fwdSender brokerTs
fwdSender = memberFwd / channelFwd
memberFwd = %s"M" memberId memberName ; attributed to specific member
channelFwd = %s"C" ; attributed to channel as sender
brokerTs = 8*8 OCTET ; UTC system time
The parser (parseChatMessages) dispatches on the first byte:
'='-> binary batch (new format, used by channels)'X'-> compressed (decompress, then re-parse)'['-> JSON array (legacy group format)'{'-> single JSON message
Forward elements contain the original message bytes verbatim. The relay does not re-encode the inner message. This is what makes signature verification possible after forwarding: the exact bytes that were signed by the owner are preserved through the relay.
Nested forwarding (> inside >) is explicitly rejected by the parser.
Delivery pipeline
The relay's delivery pipeline has two stages, both backed by persistent database tables for delivery reliability (not for authoritative storage - the relay's database is a delivery queue, not a content database):
Stage 1: Delivery tasks. When the relay receives a message from an owner that should be forwarded, it creates a delivery_task record:
delivery_task:
group_id, worker_scope, job_scope,
sender_group_member_id, message_id,
message_from_channel (bool),
task_status (new -> processed)
A task worker (one per group per scope) reads pending tasks, batches multiple tasks into a single binary batch body, and creates a delivery job.
Stage 2: Delivery jobs. A delivery job contains the pre-encoded batch body and a cursor for paginated delivery:
delivery_job:
group_id, worker_scope, job_scope,
body (pre-encoded binary batch),
cursor_group_member_id,
job_status (pending -> complete)
A job worker reads the body and delivers it to subscribers in paginated batches. For each page, it loads a bucket of subscribers by cursor position, sends the body to all of them, advances the cursor, and continues until all subscribers have been served. This avoids loading all subscribers into memory at once.
For subsequent subscribers in a batch, the agent uses a value reference to the first subscriber's message body, avoiding redundant data transmission to the SMP server.
Message deduplication
When multiple relays serve the same channel, each subscriber receives the same message from each relay independently. Deduplication is performed at the subscriber's client level using the message's shared message ID:
- When saving a received message, the client checks whether a message with the same shared ID already exists for this group.
- If a duplicate is found, the message is silently dropped (in channels with relays).
- In non-relay groups, duplicate detection triggers a
x.grp.mem.connotification to the forwarding member.
This is essentially cache coherence verification - comparing what was received from one cache node against another. TODO: Currently, deduplication only detects the presence of duplicates. The protocol design includes provisions for detecting differences between relay-delivered copies of the same message (hash comparison, UI indicators for discrepancies). This is described in the channels forwarding RFC and is not yet implemented.
Channel-as-sender messages
Owners can send messages attributed to the channel rather than to themselves. When asGroup = True is set in the message container, the relay forwards the message with a channel-as-sender tag instead of attributing it to a specific member. On the subscriber side, such messages are displayed as coming from the channel (using the channel's profile image and name) rather than from a specific owner.
This will be useful for channels with multiple owners (not yet implemented at application level) where the identity of the specific sender should not be visible to subscribers. The relay must respect this directive; ignoring it and revealing the sending owner's identity is a threat vector (detectable out-of-band by members communicating with the owner).
The forwarding binding prefix for channel-as-sender messages uses CBChannel instead of CBGroup, and includes only the channel's entity ID (not the sender's member ID):
channelBinding = smpEncode(CBChannel) <> smpEncode(publicGroupId)
Member support scope
Channels support a member support scope - a private side-channel between a subscriber and the channel's moderators/owners. Messages sent in the support scope are delivered only to moderators and the scoped subscriber, not to all subscribers.
A support-scoped message includes the target member's ID. The delivery pipeline uses a separate job scope for support messages, loading only the scoped member and moderators rather than all subscribers.
Support scope messages are visible only to the subscriber who initiated the support conversation and to the channel's moderators. Other subscribers cannot see them. This allows subscribers to report issues, appeal moderation decisions, or communicate with administrators without revealing their identity to other subscribers.