Offline-first apps and sync
Designing apps that work without a connection and sync later: local-first storage, change logs and sync cursors, conflict resolution with last-writer-wins, merges and CRDTs, operational transformation, and keeping clients and server consistent.
Reading is half of it. See this used in a real interview: walk through Design Google Docs →
Phones go through tunnels, laptops go on planes, and users expect notes, documents, messages and to-do lists to keep working and to merge correctly afterwards. Offline-first design is replication with the hardest possible replicas: devices that disappear for days and edit the same data independently. It comes up in questions about collaborative editors, file sync, chat and mobile apps.
Local first
The app reads and writes a local database (SQLite, IndexedDB, Realm) and treats the server as a sync partner, not as the source of every read:
- The UI updates instantly from local data; no spinner for every action.
- Writes are recorded locally with a pending flag and sent when a connection exists.
- The server is still the authority for permissions, validation and anything shared.
Syncing changes
The usual building blocks:
- Client-generated ids (UUIDv7 or similar) so items created offline have ids that will not collide. See unique ids, ordering and time.
- An outbox of local changes, sent in order with idempotency keys so resending after a dropped connection is safe.
- A server change log with a monotonically increasing sequence per user or workspace; the client stores a sync cursor (the last sequence it has seen) and asks "what changed since 18342?" on reconnect.
- Pagination and batching on catch-up, so a device returning after a month does not download everything in one request. See pagination.
- Tombstones for deletions, kept long enough that offline devices learn about them.
Conflicts
Two devices edit the same thing while apart. Options, from simplest to most capable:
| Approach | How it resolves | Good for | Weakness |
|---|---|---|---|
| Last-writer-wins (per record) | the later timestamp wins | settings, profile fields | silently drops the other edit; depends on clocks |
| Last-writer-wins per field | merge records field by field | forms, contacts | still drops concurrent edits to the same field |
| Server rejects and the user resolves | show both versions | files, important documents | asks the user to do the work ("conflicted copy") |
| Domain merge rules | union of sets, max of counters, append lists | carts, tags, logs | needs rules per data type |
| CRDTs | data types that merge automatically and deterministically | text, lists, counters, collaborative state | metadata overhead, harder to reason about |
| Operational transformation (OT) | a central server transforms concurrent operations | real-time text editors | needs a server in the loop; complex |
Choose per data type. Most apps combine per-field last-writer-wins for simple fields, merge rules for collections, and something richer only for text. Use a hybrid logical clock or server-assigned versions rather than device wall clocks, which can be wrong by minutes.
CRDTs and OT for collaborative text
CRDTs (conflict-free replicated data types) give each character or element a unique, ordered identity so that applying the same set of operations in any order yields the same result. They work peer-to-peer and offline, which makes them the natural fit for local-first editors (Yjs, Automerge).
OT transforms each operation against concurrent ones on a central server; Google Docs historically used it. It suits always-online collaboration with a server, and is harder to apply offline. See Design Google Docs.
Files
Large files sync in chunks (content-defined chunks, hashed), so a small edit uploads only the changed chunks and identical chunks are deduplicated. Conflicting edits to binary files usually produce a "conflicted copy" rather than a merge. See Design Dropbox.
Permissions and validation while offline
The client cannot be trusted to decide what it is allowed to do. The server validates every synced change, may reject some (permission revoked while offline, invalid state), and the client must handle rejections gracefully by rolling back the local change and telling the user.
Real-time on top
When online, a WebSocket or SSE connection pushes new changes immediately, and the same change log handles catch-up after disconnects, so there is one sync path rather than two. See real-time systems.
Pitfalls
- Relying on device clocks to order edits.
- Forgetting deletions (no tombstones) and resurrecting deleted items from an old device.
- Unbounded local storage growth; prune what the user no longer needs.
- Schema migrations: old app versions sync with new servers for months, so the sync protocol must be versioned.
Checklist
- Local database as the primary read and write path.
- Client-generated ids, an outbox with idempotency keys, a server change log with cursors.
- Conflict strategy chosen per data type; no reliance on device clocks.
- CRDTs or OT only for genuinely collaborative state.
- Server-side validation and handled rejections.
- Tombstones, chunked file sync, and a versioned sync protocol.