<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Service Worker on Samitha Widanage</title>
    <link>https://samithahansaka.com/tags/service-worker/</link>
    <description>Recent content in Service Worker on Samitha Widanage</description>
    <generator>Hugo -- 0.162.1</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 04 Jun 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://samithahansaka.com/tags/service-worker/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>React Offline Kanban: CRDT-based Offline-First Prototype</title>
      <link>https://samithahansaka.com/work/react-offline-kanban/</link>
      <pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://samithahansaka.com/work/react-offline-kanban/</guid>
      <description>A working offline-first kanban built with React, Redux Toolkit, Redux Saga, Yjs, and Hocuspocus. No sync queue, no conflict resolver. The CRDT handles concurrent edits losslessly.</description>
      <content:encoded><![CDATA[<p><strong>Public repo:</strong> <a href="https://github.com/samithahansaka/react-offline-kanban">github.com/samithahansaka/react-offline-kanban</a></p>
<h2 id="what-it-is">What it is</h2>
<p>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.</p>
<p>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&rsquo;s codebase.</p>
<h2 id="what-it-demonstrates">What it demonstrates</h2>
<ul>
<li><strong>CRDT-based conflict resolution.</strong> Yjs handles concurrent edits from multiple offline clients. No sync queue, no LWW resolver, no conflict UI.</li>
<li><strong>Real-time sync via WebSocket.</strong> The HocuspocusProvider keeps clients in sync within the same second; offline edits flush automatically on reconnect.</li>
<li><strong>Service Worker + IndexedDB.</strong> Works fully offline. Cards added during an outage survive reloads and sync when the network returns.</li>
<li><strong>Redux Saga for orchestration.</strong> Yjs owns the data; Saga owns the side effects: online detection, provider status events, UI status reporting.</li>
<li><strong>Self-hosted backend.</strong> Node.js + Hocuspocus + Postgres (Neon free tier). No managed CRDT SaaS, no vendor lock-in.</li>
</ul>
<h2 id="stack">Stack</h2>
<p><strong>Client</strong>
React 18 · TypeScript · Vite · Redux Toolkit · Redux Saga · Yjs · y-indexeddb · @hocuspocus/provider · Workbox (vite-plugin-pwa)</p>
<p><strong>Server</strong>
Node.js · @hocuspocus/server · @hocuspocus/extension-database · node-postgres · Neon Postgres</p>
<h2 id="architecture">Architecture</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">┌──────────────────────────────────────────┐
</span></span><span class="line"><span class="cl">│ Client (React)                           │
</span></span><span class="line"><span class="cl">│                                          │
</span></span><span class="line"><span class="cl">│  UI ──► Yjs document ──► y-indexeddb     │
</span></span><span class="line"><span class="cl">│   ▲           │                          │
</span></span><span class="line"><span class="cl">│   │           └─► HocuspocusProvider     │
</span></span><span class="line"><span class="cl">│   │                     │                │
</span></span><span class="line"><span class="cl">│ Redux + Saga            │                │
</span></span><span class="line"><span class="cl">│ (UI + sync state)       │ WebSocket      │
</span></span><span class="line"><span class="cl">└─────────────────────────┼────────────────┘
</span></span><span class="line"><span class="cl">                          │
</span></span><span class="line"><span class="cl">                          ▼
</span></span><span class="line"><span class="cl">┌──────────────────────────────────────────┐
</span></span><span class="line"><span class="cl">│ Server (Node + Hocuspocus)               │
</span></span><span class="line"><span class="cl">│                                          │
</span></span><span class="line"><span class="cl">│  Hocuspocus ──► Database ext ──► pg ──►  │
</span></span><span class="line"><span class="cl">│                                  Neon    │
</span></span><span class="line"><span class="cl">└──────────────────────────────────────────┘
</span></span></code></pre></div><p>The client connects via WebSocket. The server holds an authoritative Y.Doc per board name and persists it as binary to a single <code>yjs_documents</code> Postgres table on every change (debounced). When a client reconnects after being offline, the provider syncs the diff automatically.</p>
<h2 id="why-this-matters">Why this matters</h2>
<p>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&rsquo;s change is silently dropped.</p>
<p>With Yjs, both clients&rsquo; operations merge deterministically and losslessly. The sync protocol is the standard <code>y-protocols</code> exchange. The pattern generalizes well beyond kanban. Anywhere you&rsquo;d otherwise be hand-rolling a sync queue and a conflict resolver, a CRDT layer probably saves you most of that code.</p>
<h2 id="try-it">Try it</h2>
<p>Clone the repo, sign up for a free Neon project (no credit card), paste the connection string into <code>server/.env</code>, and run <code>npm run install:all &amp;&amp; npm run dev</code>. Open in two browser tabs, take one offline, edit cards in both, then bring the offline one back online. The edits merge.</p>
<p><a href="https://github.com/samithahansaka/react-offline-kanban#getting-started">Full setup steps in the README →</a></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
