Use secure durable chat
Citadel chat is durable first and has live delivery across authenticated cluster
nodes. A mutation and its bounded delivery-outbox row commit together before
Citadel attempts reliable KIND_CHAT_EVENT fan-out. The event stream is
at-least-once, not a replacement for history: deduplicate it by
(channel_id, event_id) and use history after reconnect or reconciliation.
Runnable order
Section titled “Runnable order”- Authenticate the player normally. The RPC actor is derived from that session; never send a user or sender id in chat JSON.
- Call
chat.joinwith a server-derived target. It returns an opaquechannel_id, local presence snapshot, andwatermark_event_id. Joining the same channel again from the same connection is idempotent. - Register the released SDK’s inbound handler for
KIND_CHAT_EVENT(28) before rendering live activity. Its UTF-8 JSON body has atype:presence.join,presence.leave,typing,message.create,message.update,message.remove,access.revoked, orresync_required. - Use only that returned
channel_idwithchat.send,chat.history,chat.edit,chat.delete, andchat.moderate. A copied id grants no access: Citadel reauthorizes the connection on every operation. - Pass kind 28 to the SDK’s typed chat dispatcher. It deduplicates durable
(channel_id, event_id)values, fences disconnect/revocation, and starts one bounded reconciliation generation for a gap orresync_required. Apply the complete replacement snapshot through the supplied opaque operation; the SDK sends the correlated acknowledgement only after that application succeeds. - Call
chat.leavewhen the chat view no longer needs delivery. Disconnects, blocks, group kicks/leaves/deletes, and room-access revocation remove local subscriptions and emit the appropriate presence/revocation event. Remote delivery retries within its bounded outbox deadline; afterward history is the recovery path.
Expected result: chat.join returns one opaque channel_id; a permitted
chat.send commits a message and each joined recipient on a current cluster
lease receives a
KIND_CHAT_EVENT. If a private join returns CHAT_UNAVAILABLE, treat it as a
normal unavailable target—do not reveal whether friendship, membership, or a
block caused it. On resync_required, fetch history and acknowledge its returned
watermark through the SDK’s snapshot-application operation before trusting the
live view again.
{ "target": { "kind": "direct", "other_user_id": "player-b" }}chat.join supports { "kind":"direct", "other_user_id":"player-b" },
{ "kind":"group", "group_id": 42 }, and { "kind":"room" } (the
caller’s current gateway room). Direct chat needs mutual friendship and no block
in either direction. Group chat needs current membership; room chat needs
current room presence. Citadel returns CHAT_UNAVAILABLE for every unavailable
private target, so clients must not infer which condition failed.
The old { "channel", "channel_type" } payload is rejected with
CHAT_PROTOCOL_UPGRADE_REQUIRED. After joining, target is also rejected from
all subsequent chat calls; use the opaque channel_id instead. See the
chat RPC reference and
wire envelope reference
for request, response, and event fields.
Content must be non-empty valid UTF-8 text no longer than 2,048 bytes. Sends, edits, deletes, and history reads use durable multi-key limits. Author edits are allowed for five minutes; author deletes for 24 hours. Group moderation is limited to eligible group admins and superadmins; it writes a redacted durable audit record. Typing is ephemeral and source-node local; durable create, update, remove, and moderation tombstone events use the cluster outbox.