Skip to main content
The sync relay is an optional delivery tier for change events. Your Pylon machine keeps everything it handles today: the change log, the seq counter, mutations, pulls, and auth. Only the live WebSocket fan-out moves. A PylonSync Durable Object holds the client sockets (using Cloudflare’s WebSocket hibernation), keeps a bounded ring of recent events, and filters every frame per subscriber with the same policy engine the machine uses. This gives you:
  • Sockets survive deploys and restarts. Clients stay connected to the DO while the machine restarts. When the machine comes back and pushes updates, clients see a catch-up instead of a disconnect.
  • Idle machines can sleep. Live sockets no longer keep the machine awake. The DO serves reconnect catch-up from its ring without waking the origin.
  • Fan-out CPU leaves the machine. Per-subscriber filtering and broadcast run at the edge.
The machine’s own WS keeps working unchanged. The relay is additive (dual-write). Roll it out by attaching the sink first, comparing results, then pointing clients at the relay.

Server setup

Deploy the relay worker with its own config, crates/workers/wrangler.relay.toml. That config declares only the PylonSync Durable Object plus the shared secret. It is not the full wrangler.toml, which also wires D1, KV, and R2 to run all of Pylon on Workers.
The worker’s build pins [email protected] (the release that matches the worker 0.5 crate the DO targets). The Durable Object uses SQLite-backed storage (new_sqlite_classes), which works on the Workers free plan and is Cloudflare’s default for new namespaces. Verify the deploy with the smoke script. It checks routing and auth-gating without needing the secret:
Point the machine at it:
With both set, the machine does three things:
  • Pushes its manifest to the DO at boot. The DO compiles its PolicyEngine from the manifest. Until the manifest arrives, the DO delivers nothing (it fails closed).
  • Pushes every committed change event. Each push is batched, HMAC-signed, and fire-and-forget with one retry. The disk log stays the source of truth, so a dropped push degrades to a catch-up read, never data loss.
  • Serves GET /api/sync/relay-token. It authenticates the caller, enriches org roles, and mints a signed auth blob that the DO’s filter runs against.

Client setup

That is the whole client change (TS and Swift both take relay: true). Before each socket attempt, the engine fetches a fresh token from /api/sync/relay-token and dials the relay with that token and its current cursor. The DO replays the ring tail past that cursor before sending live frames. Everything else (pull, push, mutations, auth) still talks to baseUrl.

Security model

  • The subscriber’s identity travels as a machine-minted, HMAC-signed blob carrying the enriched auth context (user, roles, active org). The DO verifies the signature with the shared secret and never touches a database. Tampering with the blob, or its expiry, fails verification.
  • Filtering matches the machine’s WS hub decision for decision:
    • policy evaluated on the raw row
    • serverOnly and syncOmit fields stripped before the wire
    • visibility flips synthesized as delete tombstones
    • unscoped admin bypass
    • default-deny for entities with no policy
  • Blobs expire (15 minutes by default). The DO closes expired sockets with code 4401. The client then re-handshakes against the machine, which is how a revoked role takes effect.
  • Machine-to-DO pushes are HMAC-signed with a timestamp (±5 min replay window).

Limits

  • Change events only. Rooms, CRDT row subscriptions, and reactive queries use the machine’s own WS. Apps that use those features keep the direct connection.
  • exists(...) read policies deny at the relay. The DO has no database to resolve them against, so it fails closed. Apps whose synced entities use exists() in read policies are not relay-eligible yet.
  • Deep history stays on the machine. A reconnect cursor older than the DO’s ring closes with code 4410. The engine’s normal /api/sync/pull catch-up (which runs on every reconnect anyway) covers the gap.
  • Role changes made mid-connection take effect when the blob expires, not immediately.

Design

The full design is in docs/SYNC_DURABLE_OBJECTS_DESIGN.md in the repo. It covers why the machine keeps seq and the change log, why the DO owns delivery but never truth, and the rejected alternatives.