Realtime resilience — many transports, one leader, cached snapshots

CareerVector's realtime channel is the path collaboration takes. It must survive vendor incidents (CF or Deno regional outages), corporate firewalls, flaky office Wi-Fi, and aggressive mobile NAT. The design assumes any single transport will fail eventually and degrades through a defined ladder.

Thin byte relay

The relays do not parse Yjs sync frames. They fan out the canonical Yjs update bytes verbatim from the writer to every connected peer. D1 is the durable source of truth — the broker is a stateless byte conduit.

This is non-negotiable: the broker has no schema knowledge, so adding a new op kind in @cv/mutations never requires a relay deploy. See REALTIME-DESIGN.md for the full broker contract.

Multi-vendor relay walker

ui/src/lib/realtime/relay-walker.ts holds a tiered URL list:

  1. CF WebSocketwss://cf.relay.careervector.corbet.ch/ws/{wsId} — the Durable Object broker. Default.
  2. Deno WebSocketwss://deno.relay.careervector.corbet.ch/ws/{wsId} — the Deno Deploy fallback. Independent vendor, independent runtime.
  3. WebRTC mesh — peer-to-peer via Y-WebRTC signalling, used when no relay is reachable but other peers are.
  4. Polling — last-resort delta polling against the sub-doc snapshot endpoint.

The walker promotes one transport at a time and demotes on failure. Quality metrics (latency, drop rate) feed the demotion decision; a previously failed transport is re-tried on a backoff so a transient outage is not permanent.

Sub-doc edge cache

Each workspace sub-doc (settings, layout, order-state, jobs, cv_profile.<lang>, cl_profile.<lang>, notes, cloud_keys) carries an independent snapshot clock. The snapshot endpoint GET /workspaces/:id/sub/:subDoc?since=N is cacheable at the edge when N matches the current clock — the response is public, immutable, max-age=86400.

A write bumps the clock, which produces a fresh URL whose response replaces the prior cache entry. Frequent writes (e.g. order_state.update) do not invalidate the rare-write cache entries (e.g. settings.update) because each sub-doc has its own URL space and its own clock.

The cell-origins projection in lib/domain/cell-origins.ts uses the same immutable-clock pattern so a peer that joins a busy workspace can hydrate the dashboard tint map without re-running the projection.

Tab leader election

A workspace open in five browser tabs does not need five WebSocket connections, five process-request claimers, and five typing-debounce timers. ui/src/lib/realtime/changes-stream-client.ts elects one tab as the leader per workspace using BroadcastChannel. The leader holds the WebSocket and fans changes out to follower tabs over the BroadcastChannel; followers re-elect on leader close.

The same leader is the default browser backend for the process-request queue. A follower tab is a passive viewer until it wins re-election.

What this gives us

  • Vendor resilience — a CF Workers outage degrades to Deno and then to WebRTC, all under the same broker contract.
  • Cost discipline — most reads of stable sub-docs come from the edge, not the Durable Object. The realtime channel only carries deltas.
  • No five-tab fan-in — leader election keeps connections, claims, and outgoing writes singular per workspace, per browser.
  • Predictable replicationcas-snapshot-clock plus the broker's byte-relay contract gives peers a strict ordering without the broker ever having to read a frame.
Source: wiki/content/canon/cv-realtime-resilience.md