Offline-First Sync with IndexedDB & Service Worker0%

Offline-First Sync with IndexedDB & Service Worker

Beginner12 min read•Updated: Jul 11, 2026
Study Materials

Capstone Part 3: Offline-First Sync with IndexedDB

In modern enterprise web development, an application should never show a dinosaur game or crash when the user loses Wi-Fi. In this capstone lesson, you will build an Offline-First Synchronization Engine that persists state locally to IndexedDB, queues optimistic mutations while offline, and synchronizes with the remote cloud when the network recovers.


1. Offline-First Architecture

Output
UI Action (User edits dashboard)
│
▼
1. Optimistic Local Update (Instant UI feedback!)
2. Write to IndexedDB local database
3. Enqueue mutation in "Outbox" Store
│
Is Device Online?
┌────────┴────────┐
YES NO
│ │
▼ ▼
Send to Server Wait for window 'online' event!
│ │
└──────◄───────────┘

2. Implementing the Offline Sync Queue Manager

JavaScript
class OfflineSyncManager {
constructor(db, apiEndpoint) {
this.db = db;
this.apiEndpoint = apiEndpoint;
this.isSyncing = false;
 
this.initNetworkListeners();
}
 
initNetworkListeners() {
// Listen for network restoration
window.addEventListener('online', () => {
console.log('[Network] Internet connection restored! Triggering sync...');
this.syncOutbox();
});
 
window.addEventListener('offline', () => {
console.warn('[Network] Device went offline. Mutations queued locally.');
});
}
 
// Enqueue an action to be executed remotely
async queueMutation(mutation) {
const record = {
id: 'mut_' + crypto.randomUUID(),
type: mutation.type,
payload: mutation.payload,
createdAt: Date.now(),
retryCount: 0
};
 
// Save to IndexedDB 'outbox' store
await this.writeToStore('outbox', record);
 
// If currently online, trigger sync immediately
if (navigator.onLine) {
this.syncOutbox();
}
}
 
async syncOutbox() {
if (this.isSyncing || !navigator.onLine) return;
this.isSyncing = true;
 
try {
const pendingMutations = await this.getAllFromStore('outbox');
console.log(`[Sync] Processing ${pendingMutations.length} pending mutations...`);
 
for (const mutation of pendingMutations) {
try {
// Dispatch to remote server
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mutation)
});
 
if (response.ok) {
// Success! Remove from outbox
await this.deleteFromStore('outbox', mutation.id);
console.log(`[Sync] Mutation ${mutation.id} synced successfully.`);
} else if (response.status >= 400 && response.status < 500) {
// Client error (unrecoverable): discard to prevent outbox blocking
await this.deleteFromStore('outbox', mutation.id);
}
} catch (netErr) {
console.warn('[Sync] Network error during batch; halting sync until online.');
break; // Stop loop; will retry on next 'online' event
}
}
} finally {
this.isSyncing = false;
}
}
 
// IndexedDB Promise Helpers
writeToStore(storeName, data) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([storeName], 'readwrite');
tx.objectStore(storeName).put(data);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
 
getAllFromStore(storeName) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([storeName], 'readonly');
const req = tx.objectStore(storeName).getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
 
deleteFromStore(storeName, key) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([storeName], 'readwrite');
tx.objectStore(storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
}

3. Background Sync API (PWA Standard)

Modern Service Workers support the Background Sync API, allowing the browser to wake up in the background and drain the outbox even after the user has completely closed the website:

JavaScript
// Register Background Sync in Service Worker
async function registerBackgroundSync() {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('outbox-sync');
console.log('Background Sync registered!');
}
}
 
// Inside sw.js (Service Worker):
self.addEventListener('sync', (event) => {
if (event.tag === 'outbox-sync') {
event.waitUntil(drainOfflineOutbox());
}
});

Interactive Quiz

Practice Quiz

Question 1
What is an "Optimistic UI Update"?
Question 2
What event fires on the window object when a device regains internet access?
Question 3
Why is an "Outbox" Object Store used in IndexedDB for offline synchronization?
Question 4
What is the primary benefit of the Service Worker Background Sync API?
Question 5
What should happen to a queued mutation in the outbox if the server returns HTTP 400 Bad Request?
Next Lesson

Production Bundling, Testing & Deployment

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext