The single biggest difference between a mobile app that feels great and one that feels janky is usually whether it was built offline-first. The janky app shows spinners every time you tap. The great app responds instantly, and the network catches up in the background. The user on the subway, in the elevator, on the airplane — they can’t tell the difference between online and offline.
Offline-first isn’t a feature you bolt on. It’s an architecture you choose at the start. Here’s what it looks like.

The core principle
The UI never waits on the network.Every read comes from a local database. Every write goes to the local database first, immediately, and is queued for sync. The network is an optimization that keeps devices in agreement — not a requirement for the app to function.
This inverts the usual model. In a network-first app, a tap fires a request, shows a spinner, waits for the response, then updates the UI. In an offline-first app, a tap updates the local DB (instant), the UI re-renders from local data (instant), and a background process syncs the change when it can.
The three layers
1. Local-first database
SQLite, WatermelonDB, Realm, or a sync-native option like PowerSync or ElectricSQL. Every read and write hits this first. The app treats it as the source of truth for the UI. This is what makes the app feel instant.
2. Sync engine
Queues local mutations, sends them to the server when there’s a connection, pulls remote changes down, and resolves conflicts. This is the hard part — and the part you should use a library for rather than hand-rolling.
3. Server
The authoritative source of truth across all devices. Validates incoming mutations, performs the canonical merge, and broadcasts changes to other devices. It’s still essential — it’s just no longer in the critical path of every user interaction.
The hard part: conflict resolution
Two devices edit the same record while both offline. They reconnect. Who wins? There’s no universally correct answer, but there are good defaults:
- Last-write-wins (LWW). Simplest. The most recent edit (by timestamp) wins. Fine for many fields; lossy for collaborative editing.
- CRDTs (conflict-free replicated data types). Mathematically guaranteed to merge without conflicts. Great for collaborative text, lists, counters. More complex to implement; libraries (Yjs, Automerge) help.
- Field-level merge.If device A changed the title and device B changed the description, merge both. Avoids one user clobbering the other’s unrelated change.
- Explicit conflict UI. When automatic merge is risky, surface the conflict to the user. Rare, but the right call for high-stakes data.
Pick the strategy per data type. A counter wants CRDT; a settings toggle is fine with LWW; a shared document wants field-level or CRDT.
What you get for the effort
- Instant UI. No spinners on every tap. The app feels native-fast.
- Works on bad networks. Subway, elevator, rural, airplane — the app keeps working.
- Resilient to flaky connections. A dropped request doesn’t lose the user’s work; it retries.
- Lower server load. Reads come from local DB; the server only handles sync.
When NOT to go offline-first
- Data must be real-time-consistent(live trading, real-time bidding) — offline-first’s eventual consistency is wrong for these.
- Data is too large to store locally(a video library, a massive dataset) — though you can be offline-first for metadata and stream the heavy content.
- The app is fundamentally a thin clientto a server-side process — if there’s nothing meaningful to do offline, the architecture is overhead.
How we approach this
For mobile apps we ship via UI/UX Design and our product engineering practice, offline-first is the default for anything where users create or edit data on the go. We use a sync-native local database and pick conflict-resolution strategy per data type — never hand-roll the sync engine.
Takeaways
- The UI never waits on the network. Local DB is the source of truth for the UI.
- Three layers: local DB, sync engine, server.
- Conflict resolution is the hard part — pick a strategy per data type.
- Use a sync library; don’t hand-roll the engine.
- Not for real-time-consistent or thin-client apps.







