Offline-First Sync with IndexedDB & Service Worker0%
Offline-First Sync with IndexedDB & Service Worker
Beginner12 min read•Updated: Jul 11, 2026
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.
Related Lessons
| Previous Lesson | Next Lesson |
|---|---|
| Real-Time Collaborative Dashboard Engine | Production Bundling, Testing & Deployment |
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.