Public repo: github.com/samithahansaka/react-offline-kanban
What it is
A small but production-shaped React app that demonstrates offline-first sync without writing any conflict resolution code. Cards persist to IndexedDB locally; concurrent edits across tabs or devices merge automatically through a Yjs CRDT; a self-hosted Hocuspocus server backed by Neon Postgres holds the authoritative state.
Built as a public reference implementation of the offline-first patterns I work with at scale, but using a different paradigm (CRDT instead of last-write-wins) so the code is genuinely standalone and unrelated to any employer’s codebase.
What it demonstrates
- CRDT-based conflict resolution. Yjs handles concurrent edits from multiple offline clients. No sync queue, no LWW resolver, no conflict UI.
- Real-time sync via WebSocket. The HocuspocusProvider keeps clients in sync within the same second; offline edits flush automatically on reconnect.
- Service Worker + IndexedDB. Works fully offline. Cards added during an outage survive reloads and sync when the network returns.
- Redux Saga for orchestration. Yjs owns the data; Saga owns the side effects: online detection, provider status events, UI status reporting.
- Self-hosted backend. Node.js + Hocuspocus + Postgres (Neon free tier). No managed CRDT SaaS, no vendor lock-in.
Stack
Client React 18 · TypeScript · Vite · Redux Toolkit · Redux Saga · Yjs · y-indexeddb · @hocuspocus/provider · Workbox (vite-plugin-pwa)
Server Node.js · @hocuspocus/server · @hocuspocus/extension-database · node-postgres · Neon Postgres
Architecture
┌──────────────────────────────────────────┐
│ Client (React) │
│ │
│ UI ──► Yjs document ──► y-indexeddb │
│ ▲ │ │
│ │ └─► HocuspocusProvider │
│ │ │ │
│ Redux + Saga │ │
│ (UI + sync state) │ WebSocket │
└─────────────────────────┼────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Server (Node + Hocuspocus) │
│ │
│ Hocuspocus ──► Database ext ──► pg ──► │
│ Neon │
└──────────────────────────────────────────┘
The client connects via WebSocket. The server holds an authoritative Y.Doc per board name and persists it as binary to a single yjs_documents Postgres table on every change (debounced). When a client reconnects after being offline, the provider syncs the diff automatically.
Why this matters
In a traditional offline-first design, you queue every mutation locally and replay it against the server when you reconnect. If two clients change the same field while offline, the server picks a winner by timestamp and the loser’s change is silently dropped.
With Yjs, both clients’ operations merge deterministically and losslessly. The sync protocol is the standard y-protocols exchange. The pattern generalizes well beyond kanban. Anywhere you’d otherwise be hand-rolling a sync queue and a conflict resolver, a CRDT layer probably saves you most of that code.
Try it
Clone the repo, sign up for a free Neon project (no credit card), paste the connection string into server/.env, and run npm run install:all && npm run dev. Open in two browser tabs, take one offline, edit cards in both, then bring the offline one back online. The edits merge.