Skip to content

SyncInbox Durable Object

src/durable-objects/sync-inbox.ts defines SyncInbox, the project’s first and only Durable Object (ADR-011). Each authenticated user connects to one inbox keyed by their verified userId; the inbox fans a tiny SyncSignal (“surface X changed”) to that user’s open sockets.

The diagram below shows the DO’s two entry points — the upgrade route and the fan-out RPC — and what they touch.

flowchart LR
  CLC["createLiveConnection()<br/>live.ts"] -->|"WS upgrade"| RT{{"/api/v1/sync/stream<br/>Clerk auth + CSWSH check"}}
  RT -->|"x-user-id stub"| F["SyncInbox.fetch()<br/>ctx.acceptWebSocket()"]
  F --> DO[["SyncInbox (per user)<br/>ephemeral conns only"]]
  WR["Worker write routes<br/>notifyUsers / notifyList"] -->|"RPC"| N["SyncInbox.notify(signal)"]
  N --> DO
  DO -->|"ws.send(SyncSignal)"| CLC
  SIG["src/lib/sync/signal.ts<br/>SyncSignal, MAX_CONNS, CLOSE_CODES"] -.-> DO
  D1[("D1 — source of truth")] -.->|"no business data in DO"| DO
  • fetch(request) accepts the WebSocket upgrade and calls this.ctx.acceptWebSocket(server)hibernatable WebSockets (not server.accept()), so idle inboxes pause duration billing (constitution §8.4). Idle cost ≈ 0 is the economic point of the design.
  • Connection cap — at most MAX_CONNS open sockets per user; on a new connection beyond the cap the oldest sockets are evicted with CLOSE_CODES.CONNECTION_CAP (multi-tab soft cap).
  • notify(signal) — the RPC the Worker calls to broadcast a SyncSignal to all open sockets.
  • webSocketClose / webSocketError — lifecycle hooks that clean up connection state.

A new connection (with cap enforcement) and a later fan-out flow through the DO like this:

sequenceDiagram
  participant C as "Client (live.ts)"
  participant R as "/sync/stream route"
  participant DO as "SyncInbox.fetch()"
  participant W as "Worker write route"
  C->>R: "WS upgrade"
  R->>R: "Clerk auth + Origin/CSWSH check"
  R->>DO: "forward to user stub (x-user-id)"
  Note over DO: "if open >= MAX_CONNS: evict oldest (CONNECTION_CAP)"
  DO->>DO: "ctx.acceptWebSocket(server) — hibernatable"
  DO-->>C: "101 Switching Protocols"
  W->>DO: "notify(signal) RPC"
  DO->>C: "ws.send(SyncSignal) to all open sockets"
  C->>C: "delta read + reconcile"
  • The DO holds ephemeral connection state only — no business data; D1 remains the single source of truth (constitution §7.10).
  • It is unreachable except through the authenticated /api/v1/sync/stream upgrade route, which runs Clerk auth and an Origin/CSWSH check before forwarding to the user’s stub (constitution §7.12).
  • The SyncSignal shape and MAX_CONNS/CLOSE_CODES constants are shared via src/lib/sync/signal.ts.

Related: Worker API & Clerk Auth (the upgrade route + auth), LiveConnection WS Client (the browser end), DB Schema & Notify-Affected (who gets notified).