AI Shows an Error Page When the Network Drops
AI generates applications with: network dependency for every operation (fetch fails = feature broken), no local data storage (all state lives on the server, nothing available offline), error messages on network failure ("Network error. Please try again." on every interaction), no queuing of offline actions (the user cannot do anything until the network returns), and no sync mechanism (data entered offline is lost when the page refreshes). The app is a remote terminal for the server — useless without a connection.
Offline-first apps are: local-primary (data lives locally first, syncs to the server when possible), always functional (read cached data, write to local store, sync later), conflict-resolved (local changes and server changes merged automatically), connectivity-aware (UI indicates online/offline status without blocking interactions), and sync-resilient (background sync reconciles local and server state, handles conflicts). AI generates none of these.
These rules cover: local-first data architecture, IndexedDB as the primary data store, sync engine patterns, conflict resolution strategies, optimistic UI for offline actions, and connectivity-aware UX design.
Rule 1: Local-First Data Architecture
The rule: 'Write to local storage first, sync to the server second. Every read: query the local store (instant, available offline). Every write: save to local store immediately (instant, available offline), then queue a sync operation. The local store is the source of truth for the UI. The server is the source of truth for persistence and cross-device sync. This inversion (local-first, not server-first) means: the app works identically whether online or offline. The network affects sync speed, not functionality.'
For the data flow: '(1) User creates a note: save to IndexedDB immediately (note visible in the UI instantly). (2) Queue a sync operation: { action: "create", data: note, timestamp }. (3) If online: sync immediately (POST to server, mark as synced). (4) If offline: the queue waits. (5) When online again: process the queue in order (create, update, delete). (6) On sync conflicts: resolve with the chosen strategy (last-writer-wins, merge, or prompt user). The user experience is identical online and offline — the only difference is sync delay.'
AI generates: const notes = await fetch('/api/notes').then(r => r.json()) — every read hits the server. Offline: the fetch fails, the UI shows an error, the feature is broken. Local-first: const notes = await db.notes.toArray() — reads from IndexedDB. Always instant, always available. The server sync runs in the background. The user never waits for the network to read their own data.
- Write local first, sync to server second — local store is UI source of truth
- Every read from IndexedDB: instant, offline-available, no network dependency
- Every write to IndexedDB + sync queue: immediate UI update, background sync
- App works identically online and offline — network affects sync speed, not functionality
- Server is persistence + cross-device truth; local is UI + instant access truth
Server-first: fetch fails = feature broken. Local-first: read from IndexedDB (instant, always available), write to IndexedDB + sync queue. The app works identically online and offline. The only difference is how quickly changes reach the server.
Rule 2: IndexedDB as Primary Local Store
The rule: 'Use IndexedDB for offline data storage. IndexedDB provides: structured data storage (objects with indexes, not just key-value), large capacity (hundreds of MB, sometimes GB — vs localStorage 5-10MB limit), async API (does not block the main thread), indexes for querying (find notes by title, filter by date, sort by updated), and transaction support (atomic multi-object updates). Library: Dexie.js wraps IndexedDB with a developer-friendly API: const db = new Dexie("myapp"); db.version(1).stores({ notes: "++id, title, updatedAt" }).'
For Dexie.js usage: 'Define schema: db.version(1).stores({ notes: "++id, title, updatedAt, syncStatus" }). Create: await db.notes.add({ title, content, updatedAt: new Date(), syncStatus: "pending" }). Query: await db.notes.where("syncStatus").equals("pending").toArray() (find all unsynced notes). Update: await db.notes.update(id, { content: newContent, syncStatus: "pending" }). Dexie handles: schema versioning (migrations when the schema changes), indexing (efficient queries), and type safety (with Dexie TypeScript support).'
AI generates: localStorage.setItem('notes', JSON.stringify(notes)) — synchronous (blocks main thread), 5MB limit (one large dataset and it fails), no indexing (must parse the entire array to find one item), and no schema (raw JSON with no structure). IndexedDB with Dexie: async, hundreds of MB, indexed queries, schema versioning. The same local storage concept, built for real applications instead of toy demos.
Rule 3: Sync Engine for Background Reconciliation
The rule: 'Build or use a sync engine that reconciles local changes with the server. The sync engine: (1) monitors online/offline status (navigator.onLine + fetch health checks), (2) processes the sync queue when online (in order, with retry on failure), (3) pulls server changes (changes from other devices or users), (4) merges local and remote changes (conflict resolution), (5) updates the local store with the merged result. Libraries: Replicache (sync engine for web apps), PowerSync (Postgres sync), or WatermelonDB (React Native offline-first).'
For the sync queue: 'Table: sync_queue { id, action (create/update/delete), collection, recordId, data, timestamp, retryCount }. Processing: while (queue.length > 0 && navigator.onLine) { const op = queue[0]; try { await syncToServer(op); queue.shift(); } catch { if (op.retryCount < 5) { op.retryCount++; await delay(backoff(op.retryCount)); } else { moveToFailedQueue(op); } } }. The queue is persistent (IndexedDB) — it survives page refreshes and browser restarts. Pending operations are never lost.'
AI generates: no sync mechanism. Data written offline exists only in the browser. Page refresh: data lost. Close the browser: data lost. Switch devices: no data. With a sync engine: local data is persistent (IndexedDB), queued operations sync when online, and the server receives every change eventually. The sync engine is the bridge between local-first convenience and server-side durability.
Rule 4: Conflict Resolution Strategies
The rule: 'When the same record is modified both locally and on the server, a conflict exists. Strategies: (1) Last-writer-wins (LWW): the most recent timestamp wins. Simple, no user intervention, but loses the earlier change. Acceptable for: user preferences, non-critical settings. (2) Field-level merge: compare field by field — if different fields changed, merge both. If the same field changed, use LWW for that field. Better for: documents, profiles (title changed locally, description changed remotely = merge both). (3) CRDT: conflict-free by mathematical guarantee (see realtime collaboration article). Best for: collaborative editing. (4) User prompt: show both versions, let the user choose. Best for: critical data where automatic resolution might lose important changes.'
For field-level merge: 'Record locally: { title: "New Title", content: "old content", updatedAt: 10 }. Record on server: { title: "old title", content: "New Content", updatedAt: 12 }. Merge: title changed locally (use local: "New Title"), content changed remotely (use remote: "New Content"). Result: { title: "New Title", content: "New Content" }. Both changes preserved. If both changed title: LWW for title field (server timestamp 12 > local timestamp 10, server wins). Conflict resolution at field granularity minimizes data loss.'
AI generates: no conflict detection. Local change overwrites server change (or vice versa) silently. User A edits title offline. User B edits content online. User A comes online: their title change overwrites User B content change (the entire record is replaced). With field-level merge: both changes are preserved. The title from A and the content from B are merged into the final record. Zero data loss for non-conflicting fields.
- Last-writer-wins: simple, timestamp-based, acceptable for preferences and settings
- Field-level merge: compare per field, merge non-conflicting, LWW for conflicting fields
- CRDT: mathematically conflict-free — best for collaborative editing (see Yjs article)
- User prompt: show both versions for critical data — human decides
- Field granularity: minimizes data loss vs whole-record replacement
Whole-record replacement: User A edits title offline, User B edits content online. A comes online: title change overwrites B content change. Field-level merge: title from A + content from B = both preserved. Conflict resolution at field granularity minimizes data loss.
Rule 5: Connectivity-Aware UX
The rule: 'Show connectivity status and sync state in the UI without blocking interactions. Online indicator: subtle green dot or "Synced" text in the header — not a modal or banner. Offline indicator: amber dot or "Offline — changes will sync when connected" — informational, not blocking. Pending sync: "3 changes pending" badge. Sync in progress: subtle spinner near the status indicator. Sync error: "1 change failed to sync" with a retry option. The user should always know their sync state but never be blocked by it.'
For optimistic UI patterns: 'On create: the item appears in the list immediately (from local store) with a subtle sync indicator (cloud icon with arrow). On sync success: the indicator changes to a checkmark. On sync failure: the indicator changes to a warning icon with a retry option. On delete: the item disappears immediately (soft-deleted locally), syncs the delete to the server. If sync fails: the item reappears with an error indicator. The UI is always responsive. Sync state is supplementary information, not a blocking condition.'
AI generates: a loading spinner while fetching data (blocks the UI until the network responds) and an error modal when offline ("Cannot connect to server. Please check your internet connection." with no way to dismiss or continue). The user cannot use the app without a network. Connectivity-aware UX: the app works, the status bar shows "Offline," and the user continues working. When online: changes sync silently. The network is a feature, not a requirement.
AI shows a blocking error modal when offline. User cannot do anything. Connectivity-aware UX: subtle 'Offline' indicator, all features work, changes queue for sync. When online: silent background sync. The network enhances the experience — it does not gate it.
Complete Offline-First Rules Template
Consolidated rules for offline-first apps.
- Local-first: write to IndexedDB first, sync to server second — UI always instant
- IndexedDB with Dexie.js: structured, indexed, async, hundreds of MB — not localStorage
- Sync engine: persistent queue, background processing, retry with backoff
- Conflict resolution: LWW for simple, field-level merge for documents, CRDT for collab
- Optimistic UI: create/update/delete appear instantly, sync state as supplementary indicator
- Connectivity status: subtle indicator (dot/text), never blocking modal or error page
- Pending changes badge: '3 changes pending' — user knows sync state at a glance
- Network is a feature, not a requirement: the app works offline, sync enhances it