JSON Offline-First Sync: Local-First Applications & Conflict Resolution
Last updated:
Overview
Offline-first applications work fully offline and synchronize with the server when connectivity returns. This guide covers local-first JSON architecture, storing data in IndexedDB or SQLite, designing conflict resolution strategies (operational transform, last-write-wins, multi-way merge), implementing sync protocols, handling partial failures, and choosing between libraries like WatermelonDB, RxDB, and Replicache.
Key Patterns
- Local-First Architecture: All writes go to local storage immediately; server becomes an async replica
- IndexedDB Schema: Object stores per entity type with indexes for querying (e.g., Todo store with createdAt index)
- Sync Queue: Separate store for operations not yet acknowledged by server (tombstones for deletes)
- Conflict Detection: Compare server version with local version to detect concurrent edits
- Conflict Resolution: Last-write-wins (timestamp), operational transform, or multi-way merge (3-way diff)
- Tombstones: Mark deletes instead of removing, so synced peers know the deletion happened
- Version Vectors: Track causality when multiple devices can edit the same document
Best Practices
- Use IndexedDB for client-side storage on web; SQLite for native apps — both provide queryable JSON without server
- Design schema with separate object stores for each entity type (todos, notes, tags) for independent sync
- Implement a sync queue to track which operations have not yet reached the server — critical for resuming after network failure
- Use tombstones (mark-deleted flag) instead of immediate deletion to ensure deletes propagate to all peers
- Choose conflict resolution strategy based on data semantics: last-write-wins for simple fields, operational transform for text, 3-way merge for structured data
- Track update timestamps (local UTC) and server-assigned version for conflict detection
- Implement exponential backoff for sync retries to avoid overwhelming network or server during connectivity issues
- Use version vectors for causality tracking when multiple devices can edit the same document — prevents lost updates
- Sync in batches (50–100 operations per request) to minimize overhead
- For deletes, send tombstones to the server; let the server decide when to permanently prune old tombstones
Local-First vs Client-Server Architecture
Traditional client-server treats the server as the source of truth — offline writes fail. Local-first treats the client as the source of truth — all writes succeed immediately, and the server becomes an async replica. This provides better perceived performance (instant feedback) and offline capability. The tradeoff: complexity in sync protocols, conflict resolution, and ensuring all peers eventually converge to the same state.
IndexedDB Schema Design
IndexedDB is the browser's local database. Design it with object stores per entity type (todos, notes, categories), each with a primary key (id) and indexes for querying (created_at, updated_at, category_id). Store complete JSON objects in each store — IndexedDB serializes and deserializes automatically. Use transactions for multi-store operations to ensure consistency.
Sync Queue and Tombstones
Maintain a separate sync queue object store that tracks which operations have not yet been acknowledged by the server. When an operation fails to sync, it stays in the queue. When the server responds successfully, remove it from the queue. For deletes, use tombstones: add a mark_deleted flag and deleted_at timestamp instead of removing the row, so synced peers can propagate the deletion. The server can permanently prune old tombstones after all clients have acknowledged them (via vector clock or similar).
Conflict Resolution Strategies
For fields that both client and server modified:
- Last-Write-Wins (LWW): Use the version with the later timestamp. Simplest but loses concurrent edits. Works for independent fields (e.g., profile picture vs bio)
- Operational Transform: Represent edits as operations (insert, delete, replace) and compose them in a consistent order. Used by Google Docs. Complex but preserves intent of both edits
- CRDT (Conflict-free RDT): Mathematical structure that guarantees convergence without central coordination. Libraries like Yjs and Automerge use this
- 3-Way Merge: Compare base version, local version, and server version to automatically merge non-overlapping changes. Works well for structured data
Sync Protocol Design
Sync protocol typically works as: client sends all queued operations to server (with vector clock or version number), server detects conflicts, returns updated state for conflicted records, client applies server resolution (or reconciles using chosen strategy), client clears sync queue for acknowledged operations. Batch requests (50–100 operations per POST) to minimize round trips. Use exponential backoff on failure to avoid overwhelming the server during connectivity issues.
Version Vectors for Multi-Device Sync
When multiple devices can edit the same document, timestamps alone are insufficient because device clocks may not be synchronized. Version vectors (or vector clocks) track causality: each device has a logical counter that increments on every local edit. A record is represented as [device_id: counter_value] for each device. Concurrent edits have incomparable version vectors; causally dependent edits have one vector dominating the other. Use version vectors to detect true conflicts vs causally ordered updates.
Handling Network Failures
Network failures are transient. Sync queue survives page refreshes (persisted in IndexedDB), so failed operations automatically retry on reconnect. Implement exponential backoff: retry after 1s, then 2s, 4s, 8s, etc. up to a maximum (60s). For critical operations (payment), require explicit user retry. For less critical updates, retry automatically. Always handle the case where the sync succeeds on the server but the response is lost — idempotent operations (same operation ID) prevent duplicate processing.
Libraries and Frameworks
WatermelonDB: Local-first SQLite/IndexedDB with real-time sync and observability (works with WatermelonSync backend)
RxDB: Reactive database with offline sync, configurable conflict resolution, and plugin system for cloud sync
Replicache: Client-side database with automatic sync to JSON API backend, handles conflict resolution and deduplication
Yjs / Automerge: CRDT libraries that handle multi-way merge automatically, used in collaborative editing tools
PouchDB: CouchDB-compatible database with built-in replication (now community-maintained, not actively developed)
Testing Offline-First Sync
Test network failures explicitly: disconnect during sync, restart app during sync, corrupt a sync response, send conflicting updates from two devices simultaneously. Use DevTools network throttling to simulate slow connections. Verify that: all queued operations eventually sync, deletes propagate to all peers, no duplicate operations after failure, and conflict resolution is deterministic.
Further reading and primary sources
- WatermelonDB Documentation — Local-first database with SQLite/IndexedDB backend and sync protocol
- RxDB Documentation — Reactive database with offline sync and observable queries
- Replicache Documentation — Client-side database with automatic sync to JSON API
- Yjs Documentation — CRDT library for collaborative editing and automatic conflict resolution
- Automerge Documentation — CRDT library for complex data structures with built-in merge
- W3C IndexedDB Spec — Official IndexedDB API specification
- Operational Transformation Overview — Google Mobwrite project explaining operational transform for collaborative editing