clickclack/docs/features/realtime.md
2026-05-08 06:27:24 +01:00

3.2 KiB

read_when
changing the websocket endpoint, event hub, or cursor logic
adding a new durable event type
touching reconnect/recovery semantics

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 by workspace_id. Buffered per-subscriber channel (32 events) with non-blocking send.
  • events table — append-only log scoped to a workspace, with a sortable cursor.
  • httpapi.websocket — accepts a connection, validates membership, drains 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 /ws upgrades to a WebSocket. On connect it backfills up to 500 durable events newer than after_cursor, then streams live publishes until the client disconnects. Membership is rechecked on every connect.
  • GET /events is the same backfill in pull form. Use it after a long offline period instead of relying on the connect-time backfill.
  • POST /ephemeral publishes 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.updated
  • message.created, message.updated, message.deleted
  • thread.reply_created, thread.state_updated
  • reaction.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.started
  • typing.stopped
  • presence.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_cursor on 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 with after_cursor will fill in.

Implementation pointers

  • coder/websocket is the WebSocket library. The current accept call passes InsecureSkipVerify for the local dev case; production should put the server behind a reverse proxy that validates Origin, or harden the accept options.
  • The hub is single-process. Multi-node fanout is out of V1 scope.