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:
- CF WebSocket —
wss://cf.relay.careervector.corbet.ch/ws/{wsId}— the Durable Object broker. Default. - Deno WebSocket —
wss://deno.relay.careervector.corbet.ch/ws/{wsId}— the Deno Deploy fallback. Independent vendor, independent runtime. - WebRTC mesh — peer-to-peer via Y-WebRTC signalling, used when no relay is reachable but other peers are.
- 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 replication —
cas-snapshot-clockplus the broker's byte-relay contract gives peers a strict ordering without the broker ever having to read a frame.