Realtime
The realtime layer is a notification pipe over WebSocket plus a recovery endpoint over HTTP. SQLite is the source of truth; the websocket is allowed to drop events.
#Components
apps/api/internal/realtime/hub.go— in-process pub/sub keyed byeventstable — append-only log scoped to a workspace, with a sortableevent_recipientstable — optional per-event recipient rows for durablehttpapi.websocket— accepts a connection, validates membership, drains
workspace_id. Buffered per-subscriber channel (32 events) with non-blocking send.
cursor.
private events such as DMs and read receipts.
backlog from events, then forwards live publishes from the hub.
#Endpoints
GET /api/realtime/ws?workspace_id=&after_cursor=
GET /api/realtime/events?workspace_id=&after_cursor=&limit=
POST /api/realtime/ephemeral
GET /wsupgrades to a WebSocket. On connect it backfills up to 500GET /eventsis the same backfill in pull form. Use it after a long offlinePOST /ephemeralpublishes a non-durable typing, presence, or agent progress
durable events newer than after_cursor, then streams live publishes until the client disconnects. Membership is rechecked on every connect.
period instead of relying on the connect-time backfill. User-private durable events, such as read receipts, are filtered the same way as the WebSocket stream.
event into the hub. Channel events are scoped by channel_id; DM events must send direct_conversation_id and are delivered only to that conversation's members.
#Event shape
{
"id": "evt_...",
"cursor": "...", // sortable; opaque to clients
"type": "message.created",
"workspace_id": "wsp_...",
"channel_id": "chn_...", // omitted for workspace-wide events
"seq": 124, // present when tied to channel_seq
"created_at": "2026-05-08T12:00:00Z",
"payload": { /* type-specific */ }
}
#Durable events
Inserted in the same transaction as the underlying mutation:
channel.created,channel.updatedmessage.created,message.updated,message.deletedchannel.read,dm.readthread.reply_created,thread.state_updatedreaction.added,reaction.removedmember.moderation_updated
Direct messages also publish into the workspace event stream so DM lists stay fresh, but they are persisted with recipient rows and replay only to direct conversation members.
message.created carries the message sequence in top-level seq and includes message_id, author_id, optional direct_conversation_id, and optional nonce in payload. Read receipt events carry the updated read pointer in top-level seq and include user_id plus the channel or DM conversation ID in payload; they are delivered only to that user. Moderation events carry the target user_id and current role; they are private to the target user and current owners/moderators.
#Ephemeral events
Not persisted, not delivered after disconnect, may be dropped under load:
typing.startedtyping.stoppedpresence.changedagent.progress
For DM typing and progress, the server verifies the sender is in the direct conversation and filters WebSocket delivery to that member set. Workspace members outside the DM do not receive the event. agent.progress is bot-only and must name exactly one target, so progress from a private agent turn cannot fall back to a workspace-wide broadcast.
POST /api/realtime/ephemeral validates workspace membership and tags the payload with user_id from the caller before publishing.
#Recovery rules
- The client sends
after_cursoron every connect/reconnect. - Server returns up to 500 durable events with a higher
cursor. Anything - The websocket itself does not drop durable events — they are always in
- Operators can prune old durable events with
older than that window must be re-fetched through the HTTP API (/messages, /thread, /channels) — clients should treat the gap as "resync_required".
events. A buffered hub channel that overflows simply stops receiving live events; the next reconnect with after_cursor will fill in.
clickclack admin events prune. Message history is not stored in the event log, so clients with cursors outside the retained window should reload through the message APIs.
#Implementation pointers
coder/websocketis the WebSocket library. The accept call validates- The hub is single-process. Multi-node fanout is out of V1 scope.
Origin against the request host and configured public URL.