You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A self-operated Cloudflare Worker + D1 sync relay that lets personal devices attach to a private space and exchange encrypted row-bodies. Provides space creation, pairing registration (an authenticated device registers a pending join for its own space — this is what makes "add a third device later" possible; without it only the very first join works), device joining, device listing and removal (a single "remove device" action: revokes the token and removes membership), space deletion ("delete my data"), and two sync endpoints (pull with a cursor, push with last-write-wins) plus a long-poll notification channel. The relay never parses row bodies — it stores opaque, encrypted payloads and orders them by a per-space monotonic version stamped transactionally. It records each device's last-pull cursor and garbage-collects tombstones once every non-removed device has pulled past them (90-day safety cap). Authenticates every request by a device token (SHA-256 at rest, constant-time comparison, scoped to one space).
Where it lives: packages/sync-relay in the monorepo. Handlers are pure functions over an injected storage interface so the test suite runs in plain vitest with a SQLite-backed D1 fake — no workerd/wrangler/miniflare dependency in the agent gate. A wrangler config and a README runbook (deploy, D1 setup, manual pairing-row insertion for total-device-loss recovery) ship with the package; deploying is a human/operator step and is not validated by the implementing agent.
Acceptance criteria
packages/sync-relay exposes handlers for POST /v1/space, POST /v1/pairings, POST /v1/join, GET /v1/devices, POST /v1/devices/remove, POST /v1/space/delete, POST /v1/sync/pull, POST /v1/sync/push, and a long-poll notification channel, with a wrangler config + README runbook for the operator deployment (the deployment itself is out of the agent's scope).
Creating a space returns a device token; an authenticated device can register a pending pairing (POST /v1/pairings {join_hash}) for its own space; joining with the matching join_hash issues a device token + space_id. Pairings are single-use, TTL-bounded (15 min), and attempt-limited, with the attempt budget stored and decremented transactionally in D1 (no check-then-act race).
Each push assigns a monotonically increasing per-space version inside the same transaction as the row write (no client timestamps, no detached counter); an explicit test drives two concurrent pushers and asserts the assigned versions are unique, gapless, and ordered.
pull returns rows with version > cursor, ordered, and records the calling device's cursor; tombstones are rows with a deleted flag; a stale push is accepted-and-overwritten, never rejected.
Tombstone GC hard-deletes a tombstone only once every non-removed device's recorded cursor has passed it, with a 90-day safety cap; a test covers "second device hasn't pulled yet → tombstone retained".
Every request except join authenticates via SHA-256 of the device token with constant-time comparison and a space-scope check; removed devices' tokens are refused; POST /v1/space/delete wipes all rows for the space.
The relay stores generic rows (space, table, pk, body, version, deleted) with an index on (space, version) and never reads row bodies.
Tests cover the version counter (incl. the concurrency case), cursor semantics, LWW, token auth, pairing single-use/TTL/attempt budget, tombstone GC, and the long-poll channel — all runnable via the package's vitest target with the in-process fake (no network, no workerd).
Parent
#130
What to build
A self-operated Cloudflare Worker + D1 sync relay that lets personal devices attach to a private space and exchange encrypted row-bodies. Provides space creation, pairing registration (an authenticated device registers a pending join for its own space — this is what makes "add a third device later" possible; without it only the very first join works), device joining, device listing and removal (a single "remove device" action: revokes the token and removes membership), space deletion ("delete my data"), and two sync endpoints (pull with a cursor, push with last-write-wins) plus a long-poll notification channel. The relay never parses row bodies — it stores opaque, encrypted payloads and orders them by a per-space monotonic version stamped transactionally. It records each device's last-pull cursor and garbage-collects tombstones once every non-removed device has pulled past them (90-day safety cap). Authenticates every request by a device token (SHA-256 at rest, constant-time comparison, scoped to one space).
Where it lives:
packages/sync-relayin the monorepo. Handlers are pure functions over an injected storage interface so the test suite runs in plain vitest with a SQLite-backed D1 fake — no workerd/wrangler/miniflare dependency in the agent gate. Awranglerconfig and a README runbook (deploy, D1 setup, manual pairing-row insertion for total-device-loss recovery) ship with the package; deploying is a human/operator step and is not validated by the implementing agent.Acceptance criteria
packages/sync-relayexposes handlers forPOST /v1/space,POST /v1/pairings,POST /v1/join,GET /v1/devices,POST /v1/devices/remove,POST /v1/space/delete,POST /v1/sync/pull,POST /v1/sync/push, and a long-poll notification channel, with awranglerconfig + README runbook for the operator deployment (the deployment itself is out of the agent's scope).POST /v1/pairings {join_hash}) for its own space; joining with the matchingjoin_hashissues a device token +space_id. Pairings are single-use, TTL-bounded (15 min), and attempt-limited, with the attempt budget stored and decremented transactionally in D1 (no check-then-act race).pullreturns rows withversion > cursor, ordered, and records the calling device's cursor; tombstones are rows with adeletedflag; a stale push is accepted-and-overwritten, never rejected.joinauthenticates via SHA-256 of the device token with constant-time comparison and a space-scope check; removed devices' tokens are refused;POST /v1/space/deletewipes all rows for the space.Blocked by