Realtime
The realtime layer is a notification pipe over WebSocket plus a recovery endpoint over HTTP. SQLite is the source of truth; when live delivery cannot keep up, the server disconnects the websocket so the client can replay.
#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; overflow removes and closes only the slow subscriber.
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=&include_tail=
POST /api/realtime/ephemeral
GET /wsupgrades to a WebSocket. On connect it captures the latest visibleGET /eventsexposes durable replay in pull form. User-private durablePOST /ephemeralpublishes a non-durable typing, presence, or agent progress
durable-event cursor, pages forward from after_cursor until reaching that fixed tail, then streams live publishes until the client disconnects. Events created after the captured tail stay on the live path instead of extending replay indefinitely. Connect-time replay is capped at 5,000 events; larger gaps close with application code 4001 so the client can perform an authoritative HTTP resync. Membership is rechecked on every connect.
events, such as read receipts, are filtered the same way as the WebSocket stream. Pass include_tail=true when a fresh client needs to skip retained history: the response adds tail_cursor, captured before the page query, and the client can open /ws from that cursor without racing events created during startup. Servers that predate this option omit the field.
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.removedpin.added,pin.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. message.created and thread.reply_created also include the request's validated correlation_id when one is available. This metadata survives both cursor replay and live WebSocket delivery; it is omitted for events created outside a correlated request and never contains message bodies. 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.
While a turn is live, the web app resolves that authenticated user_id through the shared workspace identity cache and names the responding agent beside both channel and thread composers. Agent identity is workspace-visible by design to members who can receive that channel or DM progress event. Concurrent agents are keyed by (user_id, turn_id); an unresolved sender is shown as Agent.
The TypeScript SDK exports AgentProgressLine, AgentProgressPayload, and EphemeralEventInput. Its input union requires one target for typing and agent progress while retaining targetless, workspace-wide presence events.
#Recovery rules
- The client sends
after_cursoron every connect/reconnect. - On WebSocket connect, the server pages durable events with a higher
cursor - The websocket itself does not drop durable events — they are always in
- Operators can prune old durable events with
until it reaches the visible tail captured for that connection. If replay is interrupted, the client can reconnect with the last cursor it actually processed and resume from there. If the 5,000-event work budget is exhausted, the server closes with code 4001; the web client clears its stale cursor, captures a fresh tail, completes an authoritative projection resync, and then resumes live delivery.
events. If a subscriber's buffered hub channel overflows, the server closes that websocket with a retryable status and instructs the client to reconnect with its last after_cursor so durable events can be replayed. Ephemeral events are not recoverable.
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.