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.

Full setup steps in the README →