Offline Sync
Conflict Resolution — Choose One Per Data Type
| Strategy | Use when |
|---|---|
| Last-Write-Wins (LWW) | Settings, profile fields — losing concurrent edits is acceptable |
| Server-wins | Inventory, balances — server is authoritative |
| Client-wins | Purely local data (drafts, preferences) never shared |
| Field-level merge | Documents — non-overlapping fields merge; conflicts flagged |
| CRDT (Yjs, Automerge) | Collaborative text, sets, counters — guaranteed merge; high complexity |
Define the policy per collection in writing before implementation — changing it later requires a data migration.
Sync Protocol Design
- Every write gets a logical clock (Lamport or hybrid logical clock). Wall-clock alone is insufficient — clocks drift and go backward.
- Delta sync: client sends
{since: lastSyncedCursor}; server returns only changes since that cursor. Never full-table sync after initial load. - Tombstones: soft-delete with
deletedAt; never hard-delete until all clients have synced past that version. - Sync cursor: opaque, server-generated; stored per collection on the client; resumed on next sync.
Local Storage by Platform
| Platform | Recommended |
|---|---|
| Web | IndexedDB — persistent and queryable |
| iOS / macOS | Core Data, SQLite (GRDB), or Realm |
| Android | Room (SQLite) or Realm |
| Electron / Tauri | SQLite via better-sqlite3 or rusqlite |
| Cross-platform | WatermelonDB (RN + web), PowerSync, Realm |
Optimistic UI
Apply the change locally immediately; sync in background with a client-generated idempotency key. On success, reconcile if server differs. On failure, surface the error — never silently discard user edits.
Background Sync
- Web: Background Sync API (Service Worker).
- iOS:
BGProcessingTask/BGAppRefreshTask(not guaranteed — sync-on-foreground is primary). - Android: WorkManager with network constraint.
- Desktop:
setInterval+navigator.onLine/ OS connectivity events.
Testing Sync
- Partition: disable network mid-op; apply local changes; reconnect; assert server state matches.
- Concurrent edits: two clients edit offline; reconnect both; assert final state matches the conflict policy.
- Tombstone propagation: delete on client A while B is offline; reconnect both; assert B sees deletion.
- Cursor fuzz: send stale, future, malformed cursors; assert safe error or reset.
Guardrails
- Never use wall-clock time alone as a conflict arbiter.
- Never hard-delete synced records until all clients advance past the deletion cursor.
- Apply TLS and field-level encryption for sensitive fields.