3.2 KiB
3.2 KiB
| read_when | |||
|---|---|---|---|
|
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 byworkspace_id. Buffered per-subscriber channel (32 events) with non-blocking send.eventstable — append-only log scoped to a workspace, with a sortablecursor.httpapi.websocket— accepts a connection, validates membership, drains backlog fromevents, 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 500 durable events newer thanafter_cursor, then streams live publishes until the client disconnects. Membership is rechecked on every connect.GET /eventsis the same backfill in pull form. Use it after a long offline period instead of relying on the connect-time backfill.POST /ephemeralpublishes a non-durable typing/presence event into the hub.
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.deletedthread.reply_created,thread.state_updatedreaction.added,reaction.removed
Direct messages also publish into the workspace event stream so DM lists stay fresh.
Ephemeral events
Not persisted, not delivered after disconnect, may be dropped under load:
typing.startedtyping.stoppedpresence.changed
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 older than that window must be re-fetched through the HTTP API (/messages,/thread,/channels) — clients should treat the gap as "resync_required". - The websocket itself does not drop durable events — they are always in
events. A buffered hub channel that overflows simply stops receiving live events; the next reconnect withafter_cursorwill fill in.
Implementation pointers
coder/websocketis the WebSocket library. The current accept call passesInsecureSkipVerifyfor the local dev case; production should put the server behind a reverse proxy that validatesOrigin, or harden the accept options.- The hub is single-process. Multi-node fanout is out of V1 scope.