Skip to content

Relais : Worker + D1 (serveur de sync) #131

Description

@64ix

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-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).

Blocked by

  • None — can start immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions