All articles
· 9 min read

Offline-first PWA architecture that survives real networks

Patterns for building PWAs that work on planes, subways and rural 3G — cache layering, sync queues, conflict resolution and the UI cues users expect.

TI
The InstantPWA team
Product & engineering

Offline-first isn’t a Lighthouse checkbox; it’s an architecture. Done right, users on a spotty train ride experience your app as if the network never existed. Done wrong, they see silent data loss. Here’s the pattern we use for real products.

The three layers

  1. UI — always reads from local storage first, network second.
  2. Local store — IndexedDB (via idb-keyval or Dexie), keyed by the same IDs the server uses.
  3. Sync worker — Background Sync API queues writes when offline and replays on reconnect.

Reads

Never await fetch() in the render path. Read from IndexedDB, render immediately, then update in the background:

const cached = await db.get('projects');
render(cached);
fetch('/api/projects').then((r) => r.json()).then((fresh) => {
  db.set('projects', fresh);
  render(fresh);
});

Writes

Assume the network will fail. Write to IndexedDB, enqueue the mutation, register a sync:

await db.add('outbox', { op: 'update', id, payload });
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-outbox');

The service worker replays the queue when connectivity returns. Handle 409 conflicts explicitly — last-write-wins is fine for personal data, wrong for shared docs.

Conflict resolution

  • Last-write-wins — fine for personal preferences.
  • Server-authoritative merge — default for CRUD apps; return the reconciled record.
  • CRDT — required for collaborative editing. Yjs or Automerge.

UI cues that build trust

  • Toolbar dot that changes colour when offline.
  • "Saved locally — will sync" toast on each queued write.
  • Sync progress banner on reconnect.
  • Explicit "retry" for hard failures instead of silent drop.

Anti-patterns

Don’t rely on navigator.onLine alone — it lies. Test connectivity with a HEAD request to your own origin. Don’t use LocalStorage for anything larger than a few KB — it’s synchronous and blocks the main thread.

Ship your install prompt in 60 seconds

InstantPWA is one line of code. Free forever for one widget, no card required.

Get started free