Offline-First Apps: Local Storage, Sync and Conflict Resolution
Offline-first app design explained: local databases, an outbox of pending changes, delta sync with cursors, conflict resolution and background sync.
An offline-first app treats the device's local database as the primary source the UI reads and writes, and synchronises with the server in the background whenever a connection is available. Users can keep working on a plane, in a basement or on a flaky network, and their changes are never lost. The design centres on four pieces: a local store, an outbox of pending changes, incremental sync from the server, and a rule for resolving conflicts.
Architecture
Writing locally: the outbox
- A user action writes to the local database immediately, so the UI updates instantly, and appends a mutation to an outbox in the same local transaction.
- Each mutation has a client-generated unique ID (an idempotency key) and, for new records, a client-generated record ID such as a UUID, so the record can be referenced before the server knows about it.
- The sync engine sends outbox entries in order when online, retrying with backoff. The server deduplicates by mutation ID, so retries after a lost response are safe.
- Once acknowledged, the entry is removed. If the server rejects it (validation, permissions), the app must show the user and roll back or fix the local change.
Reading changes: delta sync
- The server keeps a monotonically increasing change sequence (or per-table updated-at versions) and exposes "give me changes since cursor X".
- The client stores the last cursor it applied and pulls only newer changes, in pages. A fresh install does an initial full sync, which may need to be limited to recent or relevant data.
- Deletes must be represented as tombstones in the change feed, otherwise clients never learn that a record disappeared.
- Push notifications or a real-time channel can tell the client to pull sooner, but the pull protocol stays the source of truth. See push notifications.
Conflict resolution
Two devices edit the same record while one is offline. When both sync, which wins?
| Strategy | How | Good for |
|---|---|---|
| Last-write-wins (per record) | The latest timestamp or server arrival wins | Simple settings and records rarely edited concurrently |
| Field-level merge | Merge per field, so editing the title on one device and the due date on another keeps both | Forms, task managers |
| Server authority with rejection | The server rejects stale versions; the client refetches and asks the user | Inventory, bookings, money |
| CRDTs | Data types that merge automatically and deterministically | Collaborative text, lists, counters. See CRDTs |
| Manual resolution | Keep both versions and let the user choose | Documents where intent matters |
Some operations can't be offline-first: paying, booking the last seat, or anything with a global invariant. Queue them as requests with a clear "pending" state, and confirm with the server before showing success.
Practical concerns
- Background sync: platforms limit background work to save battery; sync opportunistically when the app is opened, on connectivity changes, and through OS background tasks.
- Schema migrations for the local database must run safely on old app versions' data, and the sync protocol must tolerate clients on old versions. See mobile release engineering.
- Storage limits and eviction: decide what to keep locally and how to page in older data.
- Security: encrypt sensitive local data and wipe it on sign-out.
- UX: show sync status, pending changes and conflicts clearly, so users trust the app.
Where it’s used
Local-first storage with an outbox and field-level merges keeps editing instant across devices.
Workers record deliveries and inspections offline; data syncs when coverage returns.
Messages are written locally with client IDs and sent from an outbox, showing pending and delivered states.
In an interview
- Make the local database the UI's source of truth.
- Use an outbox with client-generated IDs and idempotent server handling.
- Pull deltas by cursor, including tombstones for deletes.
- Choose a conflict strategy per data type and keep invariant-critical actions online.
Further reading
- Local-first softwareInk & Switch
- Build an offline-first appAndroid Developers