Offline-First Design
debt(d9/e9/b9/t7)
Closest to 'silent in production until users hit it' (d9). Detection_hints.tools is empty and no specialist tool can detect the absence of offline-first architecture. The flaw only becomes visible when a real user loses connectivity — typically a field report or crash analytics spike, not during development or CI. There is no linter or SAST rule that flags 'this app has no sync queue.'
Closest to 'architectural rework' (e9). The quick_fix describes a multi-layered solution: IndexedDB writes, optimistic UI, service worker Background Sync queuing, and server-side conflict resolution with timestamps. The common_mistakes entry explicitly states 'retrofitting it to a traditional request-response API is very difficult' and that 'the sync architecture must be designed upfront.' This is not a patch or refactor — it requires replacing the fundamental data-access and mutation model across the entire application.
Closest to 'defines the system's shape' (b9). Offline-first is an architectural stance that affects every data read, every write, every API contract, conflict resolution strategy, and UI feedback pattern. The tags (service-worker, indexeddb, sync, pwa) and the why_it_matters text confirm it shapes how every feature is built. Every new feature must be designed to work within the sync model, making this a rewrite-or-live-with-it commitment.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field reveals the canonical trap: developers assume offline-first is only a reliability feature for poor-connectivity environments and therefore skip it for 'normal' apps. This directly contradicts the reality described — that zero-latency local reads benefit all users universally, including those in cities with good connectivity. The assumption that 'we have good connectivity so we don't need this' is a serious and common wrong belief that leads to architectural decisions that are very expensive to reverse.
Also Known As
TL;DR
Explanation
Offline-first inverts the traditional assumption that network availability is the default state. Instead of making network requests and showing errors when they fail, offline-first applications read from local storage first (IndexedDB, Cache API, localStorage) and synchronise with the server when connectivity is available. This requires conflict resolution strategies for data modified both locally and on the server — last-write-wins is simplest but loses data; operational transforms and CRDTs (Conflict-free Replicated Data Types) handle merging correctly. For PHP backends, offline-first typically means implementing a sync API that accepts batches of locally-generated operations with timestamps rather than a traditional REST API that expects immediate round-trips.
Common Misconception
Why It Matters
Common Mistakes
- Treating offline-first as a feature to add after launch — the sync architecture must be designed upfront; retrofitting it to a traditional request-response API is very difficult.
- Using localStorage for structured data — IndexedDB supports proper querying and larger storage limits; localStorage is synchronous and blocks the main thread.
- Not implementing conflict resolution — silently overwriting server data with local changes when both were modified loses work.
- Assuming Background Sync API is universally supported — it is Chrome-only; implement optimistic UI updates with retry queues as a universal fallback.
Code Examples
// Network-required — breaks completely offline
async function saveNote(note) {
const res = await fetch('/api/notes', {
method: 'POST',
body: JSON.stringify(note)
});
if (!res.ok) throw new Error('Failed'); // user loses work
}
// Offline-first — save locally first, sync later
async function saveNote(note) {
note.id = crypto.randomUUID();
note.syncedAt = null;
// Save to IndexedDB immediately
await db.notes.put(note);
updateUI(note); // optimistic update
// Try to sync — queue if offline
try {
await fetch('/api/notes', {method:'POST', body: JSON.stringify(note)});
await db.notes.update(note.id, {syncedAt: Date.now()});
} catch {
// Will retry via Background Sync when online
await navigator.serviceWorker.ready
.then(reg => reg.sync.register('sync-notes'));
}
}