A server-authoritative, Linear-style sync engine on Cloudflare Durable Objects, with TanStack DB as the client store.
Optimistic mutations, real-time propagation, offline with exactly-once replay, typed presence, and opt-in collaborative text — as a library, on infrastructure you already run. One Durable Object per workspace, DO SQLite as the system of record, no external database tier.
Documentation · Why cf-sync? · Architecture
// One definition, imported by both the worker and the browser:
const app = defineApp({ version: 1, schema, mutators })
// Worker — the Durable Object is the backend:
export class WorkspaceDO extends createWorkspaceDO({ app }) {}
export default { fetch: createSyncFetch<Env>({ namespace: (env) => env.WORKSPACE, authorize }) }
// Browser — typed collections and optimistic intent mutations:
const { client, collections } = createWorkspace({ url, workspaceId, app, persist: true })
collections.issues.insert({ id: ulid(), title: 'ship it' }) // optimistic, converges via the server
await client.mutate.issue.move({ id, column }) // one intent, one wire op, atomic rollback- Server-authoritative — the DO re-runs every mutator with full authority; clients are optimistic caches that always converge. No merge functions in app code.
- Optimistic by default — a mutator's
applyruns instantly on the client and authoritatively on the server; rollback, rebase, and replay are the engine's job. - Offline that survives reloads — IndexedDB mirror, durable outbox, exactly-once replay via the per-client
lastMutationIdcontract. - Schema evolution with teeth — versioned migration chains, validated at boot; a one-line CI tripwire (
checkSchemaEvolution) fails the build on a schema change without a version bump, and per-workspace drift detection backstops it at runtime. - Typed presence — declare a zod shape, get throttled live cursors and server-attested identity.
- Collaborative text where it counts — rows stay LWW;
@cf-sync/yjsadds Yjs fields in the same DO for the fields that genuinely need merging. - Testable —
createTestEngineruns the real engine semantics in plain node; the engine itself is locked by contract tests and a multi-client convergence simulation in workerd.
| Package | What it is |
|---|---|
@cf-sync/protocol |
The shared definition kit: defineApp, defineSchema, defineMutators, crudMutators, AppError — importable from both worker and browser. Wire internals (hello / push / poke schemas, frame chunking, field frames) live behind @cf-sync/protocol/internal |
@cf-sync/server |
createWorkspaceDO, worker routers with an authorize hook, admin surface, and the in-memory test engine |
@cf-sync/client |
SyncClient (socket, outbox, poke application, reconnect), the TanStack DB collection adapter, React hooks |
@cf-sync/yjs |
Collaborative-text add-on: Yjs fields in the workspace DO, with a useYjsField React hook |
React apps read collections with useLiveQuery from @tanstack/react-db — install it alongside @cf-sync/client (it is declared as an optional peer, so your package manager will flag a version pair whose pinned @tanstack/db disagrees with ours).
- Why cf-sync — positioning, non-goals, honest comparisons
- Getting started — zero to two converging tabs
- Core concepts: Defining your app · Mutations · Schema evolution · Auth & sessions
- Collaboration: Presence · Collaborative text
- Client: Reading data · Offline & persistence
- Production: Offline & persistence · Testing · Operations · Limits & costs · Troubleshooting
- API reference — SyncClient · Collections · Definition kit · Server · Test engine · Yjs fields
- ARCHITECTURE.md — the contributor doc: locked decisions and invariants, with prior-art citations
Run the docs site locally with pnpm docs:dev.
The demo is a todo app exercising every plane — rows, intents, presence, collaborative text — in ~450 readable lines:
pnpm install
cd apps/demo
pnpm dev:worker # wrangler dev on :8787 (the sync worker + Workspace DO)
pnpm dev:web # vite dev server, second terminalOpen the vite URL in two tabs; use a URL hash (#team-a) to switch workspaces.
pnpm test # protocol + client (node) and server contract/convergence tests (workerd)
pnpm typecheck
pnpm build # bundle each package to dist/ (tsdown: ESM + .d.ts)
pnpm check:packages # pack as publishing would; gate with publint + arethetypeswrongPackages are ESM-only. In the monorepo, exports point at TypeScript source; publishConfig swaps them to dist/ at pack time, and CI verifies the packed artifacts. Read ARCHITECTURE.md before changing protocol, storage schema, or sync semantics.
The full roadmap is implemented and tested: protocol core, resilience, operability, client persistence with startup replay, optimistic intent mutators, session control, presence, and Yjs fields. Packages are not yet published to npm.