๐Ÿณ IntuitiveFE
Login
โ† All concepts

Offline-First & Background Sync

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

โœˆ๏ธ Google Docs on a plane โ€” you keep writing offline. When you land and get wifi, your changes sync. Conflicts resolved automatically.

Online-first vs Offline-first

Online-first (fragile)

Default assumption: network always available. Every action requires a server round-trip. Network drops โ†’ app broken. Shows spinner. User waits. Data lost.

Offline-first (resilient)

Default assumption: network is unreliable. Every action hits local storage first. Network is an enhancement. App works always. Sync is opportunistic.

The offline-first stack

Service WorkerNetwork proxy. Intercepts fetch. Serves from cache when offline.Cache Storage API, Workbox
Background SyncQueues mutations. Replays when online. Survives page closure.Background Sync API, IndexedDB queue
IndexedDBLocal database. Full CRUD offline. Structured, queryable, transactional.Dexie.js, idb, RxDB
Conflict ResolutionMerges concurrent offline edits. Ensures convergence.Last-write-wins, OT, CRDT (Y.js, Automerge)
Sync ProtocolDelta sync on reconnect. Uploads only changed records.CouchDB replication, custom delta, PouchDB
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Service Worker: install โ†’ waiting โ†’ active. Install event precaches app shell. Fetch event intercepts requests. Activate event cleans old caches.

Network timeline

t=0sONregister('/sw.js') โ€” browser downloads and parses SW
SW: parsing
OnlineStorage: Cache Storage API

Sync strategy

Cache-first for app shell, network-first for API data

Gotcha: A new SW doesn't activate while old tabs remain open. The new SW waits. Fix: call skipWaiting() in install event + clients.claim() in activate event. Or show a 'New version available โ€” reload' banner.
Insight: The Service Worker is a persistent background script between the browser and network. It survives page navigations and persists across sessions. Once installed, it intercepts every fetch for its scope.
Explored:๐Ÿ”ง๐Ÿ”„๐Ÿ—„๏ธโš”๏ธ๐Ÿ“ถ
๐ŸŽฏ

Challenge

Step through all 5 offline-first patterns. Watch the online/offline toggle and sync status on each event.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Service Worker lifecycle โ€” install/activate/fetch in detail?

js
1// sw.js
2const CACHE_NAME = 'app-v2';
3const APP_SHELL = ['/index.html', '/app.js', '/styles.css'];
4ย 
5// 1. INSTALL โ€” precache app shell
6self.addEventListener('install', (event) => {
7 event.waitUntil(
8 caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
9 );
10 self.skipWaiting(); // activate immediately (optional)
11});
12ย 
13// 2. ACTIVATE โ€” clean old caches
14self.addEventListener('activate', (event) => {
15 event.waitUntil(
16 caches.keys().then((keys) =>
17 Promise.all(keys
18 .filter((k) => k !== CACHE_NAME)
19 .map((k) => caches.delete(k))
20 )
21 )
22 );
23 self.clients.claim(); // take control of all tabs immediately
24});
25ย 
26// 3. FETCH โ€” serve cached resources
27self.addEventListener('fetch', (event) => {
28 event.respondWith(
29 caches.match(event.request).then((cached) => {
30 return cached ?? fetch(event.request); // cache-first
31 })
32 );
33});

Q: How does the Background Sync API work?

js
1// 1. Register a sync tag when going offline
2async function submitForm(data) {
3 await idb.put('sync-queue', { id: Date.now(), data, status: 'pending' });
4ย 
5 const registration = await navigator.serviceWorker.ready;
6 await registration.sync.register('submit-forms'); // tag
7}
8ย 
9// 2. SW fires 'sync' event when online
10self.addEventListener('sync', (event) => {
11 if (event.tag === 'submit-forms') {
12 event.waitUntil(replayPendingForms());
13 }
14});
15ย 
16async function replayPendingForms() {
17 const pending = await idb.getAll('sync-queue');
18 for (const item of pending) {
19 await fetch('/api/submit', {
20 method: 'POST',
21 body: JSON.stringify(item.data),
22 });
23 await idb.delete('sync-queue', item.id); // clear on success
24 }
25}

Q: What is a CRDT and why is it used for collaborative editing?

A CRDT (Conflict-free Replicated Data Type) is a data structure where concurrent operations can be merged without conflict. CRDTs have a mathematical property: regardless of the order operations are applied, the result is the same. This makes them perfect for collaborative editing โ€” two users editing offline can merge their changes without data loss, and the merge is deterministic. Y.js (used by Notion, Loom) and Automerge are popular CRDT libraries for JavaScript.

Bug: not handling sync conflicts โ€” data loss

js
1// โœ— BAD: last-write-wins without user awareness
2async function sync(localDoc) {
3 const serverDoc = await fetch('/api/doc/1').then(r => r.json());
4 // If server version is newer, just overwrite local changes:
5 await db.put('docs', serverDoc); // Alice's offline edits LOST
6}
7ย 
8// โœ“ BETTER: detect conflict, prompt user
9async function sync(localDoc) {
10 const serverDoc = await fetch('/api/doc/1').then(r => r.json());
11 if (serverDoc.updatedAt > localDoc.syncedAt) {
12 // Show conflict UI: "Server has newer version โ€” keep yours or theirs?"
13 showConflictDialog(localDoc, serverDoc);
14 }
15}
16ย 
17// โœ“ BEST (collaborative): Y.js CRDT merge
18const ydoc = new Y.Doc();
19// Both changes are merged deterministically โ€” no data loss
20Y.applyUpdate(ydoc, serverUpdate);
21Y.applyUpdate(ydoc, localUpdate);
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Dexie.js โ€” IndexedDB with a human API

ts
1import Dexie, { type Table } from 'dexie';
2ย 
3interface Note { id?: number; title: string; body: string; updatedAt: Date; }
4ย 
5class AppDB extends Dexie {
6 notes!: Table<Note>;
7ย 
8 constructor() {
9 super('AppDB');
10 this.version(1).stores({
11 notes: '++id, title, updatedAt', // ++ = auto-increment
12 });
13 }
14}
15ย 
16const db = new AppDB();
17ย 
18// CRUD โ€” all async, returns Promises
19const id = await db.notes.add({ title: 'Offline note', body: '...', updatedAt: new Date() });
20const note = await db.notes.get(id);
21await db.notes.update(id, { body: 'Updated offline' });
22const recent = await db.notes.orderBy('updatedAt').last(10).toArray();
23await db.notes.delete(id);

Workbox โ€” Service Worker strategies

js
1import { registerRoute } from 'workbox-routing';
2import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
3import { BackgroundSyncPlugin } from 'workbox-background-sync';
4ย 
5// App shell: CacheFirst โ€” never need network for versioned assets
6registerRoute(({ request }) => request.destination === 'script',
7 new CacheFirst({ cacheName: 'scripts-v2' }));
8ย 
9// API data: NetworkFirst โ€” prefer fresh, cache fallback
10registerRoute(({ url }) => url.pathname.startsWith('/api/'),
11 new NetworkFirst({
12 cacheName: 'api-v1',
13 networkTimeoutSeconds: 3,
14 plugins: [
15 new BackgroundSyncPlugin('api-queue', {
16 maxRetentionTime: 24 * 60, // retry for 24 hours
17 }),
18 ],
19 }));
20ย 
21// Images: SWR โ€” fast + background refresh
22registerRoute(({ request }) => request.destination === 'image',
23 new StaleWhileRevalidate({ cacheName: 'images-v1' }));

Y.js โ€” CRDT for collaborative editing

js
1import * as Y from 'yjs';
2import { WebsocketProvider } from 'y-websocket';
3ย 
4// Create a shared document
5const ydoc = new Y.Doc();
6const ytext = ydoc.getText('document'); // shared text type
7ย 
8// Sync via WebSocket โ€” Y.js handles merging
9const provider = new WebsocketProvider('wss://sync.example.com', 'room-1', ydoc);
10ย 
11// Offline: changes buffered locally in Y.Doc
12ytext.insert(0, 'Hello from Alice');
13ย 
14// On reconnect: Y.js sends only the diff (encoded state vector)
15// Server merges all concurrent edits โ€” guaranteed convergence
16ย 
17// Persistence: encode Y.Doc to Uint8Array for IndexedDB
18const update = Y.encodeStateAsUpdate(ydoc);
19await idb.put('crdt-state', update);
20ย 
21// Restore from IndexedDB on next load
22const savedUpdate = await idb.get('crdt-state');
23if (savedUpdate) Y.applyUpdate(ydoc, savedUpdate);

PouchDB + CouchDB replication protocol

PouchDB is a CouchDB-compatible in-browser database that replicates to a CouchDB server automatically. It handles conflict detection, revision tracking, and sync resumption out of the box โ€” the entire offline-first stack in one library.

js
1import PouchDB from 'pouchdb';
2ย 
3const localDB = new PouchDB('myapp');
4const remoteDB = new PouchDB('https://sync.example.com/myapp');
5ย 
6// Live bidirectional sync โ€” auto-reconnects on network drop
7localDB.sync(remoteDB, { live: true, retry: true })
8 .on('change', (info) => console.log('synced:', info))
9 .on('error', (err) => console.error('sync error:', err));
10ย 
11// CRUD to localDB โ€” syncs to remote when online
12await localDB.put({ _id: 'note-1', title: 'Offline note', body: '...' });
13ย 
14// Conflict resolution: CouchDB tracks all revisions
15// Each doc has a _rev field โ€” concurrent edits create branches
16// PouchDB exposes conflicts via { conflicts: true } option
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Local IndexedDB as the source of truth; delta sync on reconnect; CRDT or version vector for conflict resolution.

New SW installs alongside the old one; waits until all tabs using the old SW are closed before activating.

CRDTs merge concurrent edits mathematically; LWW silently discards one user's work.

Background Sync fires even after the page is closed; navigator.onLine only works while the page is open.

Optimistic local writes; vector clock per record; server-side merge with conflict surfacing; incremental sync on reconnect.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is Workbox and what problem does it solve?