Offline-first mobile
An offline-first app treats the network as an unreliable peer, not a
dependency. The UI reads and writes a local database and never blocks on a
request; a separate sync layer pushes local changes and pulls remote ones.
The hard part is not caching, it is reconciling two histories that diverged.
Method
- Make the local store the single source of truth. The UI queries and
mutates SQLite, Room, Core Data, or a sync-native store (Realm,
WatermelonDB, PowerSync), and observes it reactively. No screen awaits a
network call to render. Writes commit locally first and return
immediately, so the app is fully usable at zero bars.
- Queue mutations as an ordered, durable outbox. Every local change
becomes a persisted operation record with a stable client-generated id,
a type, and a payload. A background worker drains the queue when
connectivity returns, retries with backoff, and only removes an entry on
server acknowledgment. Never drop a queued mutation because a retry
failed once.
- Sync by deltas, not full refetches. Track a server cursor or
updated_since timestamp per collection and pull only what changed.
Assign ids client-side (UUIDs) so records exist and relate before the
server ever sees them, avoiding the round-trip-for-an-id trap that
forces online-only creation.
- Choose a conflict policy per field, and write it down.
Last-write-wins is acceptable for independent scalar fields with
trustworthy timestamps; it silently destroys data on concurrently edited
text or counters. Use per-field merge for documents,
server-authoritative resolution for money and inventory, and CRDTs only
where true concurrent editing is a product requirement, since they add
real complexity.
- Surface sync state honestly without blocking on it. Show pending,
synced, and failed at the record level, not a global spinner. Let the
user keep working while a background sync runs. Escalate to the user only
for conflicts the policy cannot resolve, and present both versions
plainly.
- Handle deletes and clock skew as first-class cases. Use tombstones,
not hard deletes, so a delete propagates instead of a record
reappearing on next pull. Never trust device clocks for ordering; use
server-assigned sequence numbers or logical clocks, since a wrong phone
clock corrupts last-write-wins.
Failure modes
- Duplicate records after reconnect: mutations were keyed by server id,
which did not exist offline. Key by client id end to end.
- Resurrected deletes: a hard delete lost the tombstone on the next pull.
- Silent data loss: last-write-wins on a field two users edited at once.
Boundaries
This covers local-truth architecture and reconciliation. Transport
security and token refresh for the sync endpoint are api-security and
oauth-flows. Real-time collaborative editing at scale needs a dedicated
CRDT or OT engine beyond this method. If the app is read-mostly with rare
writes, a plain cache with revalidation may be enough; do not adopt a full
sync engine for it.
1---2name: offline-first-mobile3description: Build a mobile app where the local database is the source of truth, the UI reads and writes it directly, and a background sync engine reconciles with the server. Use when the app must work on flaky or no connectivity, or when network-coupled screens feel slow and fragile.4---56# Offline-first mobile78An offline-first app treats the network as an unreliable peer, not a9dependency. The UI reads and writes a local database and never blocks on a10request; a separate sync layer pushes local changes and pulls remote ones.11The hard part is not caching, it is reconciling two histories that diverged.1213## Method14151. **Make the local store the single source of truth.** The UI queries and16 mutates SQLite, Room, Core Data, or a sync-native store (Realm,17 WatermelonDB, PowerSync), and observes it reactively. No screen awaits a18 network call to render. Writes commit locally first and return19 immediately, so the app is fully usable at zero bars.202. **Queue mutations as an ordered, durable outbox.** Every local change21 becomes a persisted operation record with a stable client-generated id,22 a type, and a payload. A background worker drains the queue when23 connectivity returns, retries with backoff, and only removes an entry on24 server acknowledgment. Never drop a queued mutation because a retry25 failed once.263. **Sync by deltas, not full refetches.** Track a server cursor or27 `updated_since` timestamp per collection and pull only what changed.28 Assign ids client-side (UUIDs) so records exist and relate before the29 server ever sees them, avoiding the round-trip-for-an-id trap that30 forces online-only creation.314. **Choose a conflict policy per field, and write it down.**32 Last-write-wins is acceptable for independent scalar fields with33 trustworthy timestamps; it silently destroys data on concurrently edited34 text or counters. Use per-field merge for documents,35 server-authoritative resolution for money and inventory, and CRDTs only36 where true concurrent editing is a product requirement, since they add37 real complexity.385. **Surface sync state honestly without blocking on it.** Show pending,39 synced, and failed at the record level, not a global spinner. Let the40 user keep working while a background sync runs. Escalate to the user only41 for conflicts the policy cannot resolve, and present both versions42 plainly.436. **Handle deletes and clock skew as first-class cases.** Use tombstones,44 not hard deletes, so a delete propagates instead of a record45 reappearing on next pull. Never trust device clocks for ordering; use46 server-assigned sequence numbers or logical clocks, since a wrong phone47 clock corrupts last-write-wins.4849## Failure modes5051- Duplicate records after reconnect: mutations were keyed by server id,52 which did not exist offline. Key by client id end to end.53- Resurrected deletes: a hard delete lost the tombstone on the next pull.54- Silent data loss: last-write-wins on a field two users edited at once.5556## Boundaries5758This covers local-truth architecture and reconciliation. Transport59security and token refresh for the sync endpoint are api-security and60oauth-flows. Real-time collaborative editing at scale needs a dedicated61CRDT or OT engine beyond this method. If the app is read-mostly with rare62writes, a plain cache with revalidation may be enough; do not adopt a full63sync engine for it.