Offline-first mobile apps: what it really takes
An app that works in a lift, a basement or on a flight is a design decision, not a feature flag. The architecture behind apps that never wait for the network.
By Rachnova6 min read
Most apps are built for the office Wi-Fi they were developed on. Then real people use them in lifts, basements, rural areas, on flights and on congested networks, and every spinner becomes a reason to close the app.
Offline-first flips the assumption: the app works fully on the device, and the network is used to sync when it is available. We built StepBuddy, a routine app for children, this way, because a child halfway through a morning routine should never be told to wait for a connection.
Offline-first is not the same as “caches some data”
Many apps cache the last screen they loaded. That helps, but it isn’t offline-first. The difference is where the truth lives:
| Online-first with cache | Offline-first | |
|---|---|---|
| Source of truth while using the app | The server | The local database |
| Creating or editing data offline | Blocked or lost | Works normally |
| Network failure | An error screen | Nothing visible changes |
| Sync | On every action | In the background, when possible |
In an offline-first app, every read and write goes to a database on the device first. The interface never waits on the network, so it feels instant even on a good connection.
The architecture, layer by layer
A structure that holds up in production:
- A local database, such as SQLite, as the app’s working store. Every screen reads from it.
- Repositories between the interface and the data, so screens never know or care whether data came from the device or the server.
- A change queue that records each local edit, so nothing is lost if the app closes before syncing.
- A sync layer that pushes queued changes to the backend and pulls remote changes down, retrying quietly when the connection drops.
Clean separation between these layers is what keeps an offline-first app maintainable. In StepBuddy we used Clean Architecture with SQLite, repositories and local caching, with a Supabase backend on the other side of the sync.
The hard part: conflicts
Sync is easy until the same record changes in two places. A parent edits a routine on one phone while the child’s device, offline, marks a step complete. Both changes are valid. Something has to decide what the final state is.
There is no universal answer, only a rule chosen deliberately for each kind of data:
- Last write wins works for simple settings where the latest choice is the right one.
- Merge by field keeps both edits when they touch different parts of a record.
- Append-only records, such as “step completed at 07:42”, avoid conflicts entirely because nothing is overwritten.
- Ask the user, for the rare case where only a person can decide.
Designing data around append-only events wherever possible removes most conflicts before they happen. Progress, rewards and history are naturally events, so they sync cleanly.
Things that still need the network
Some features are online by nature: signing in for the first time, purchases, and anything shared with other people in real time. The goal isn’t to pretend these work offline. It is to make them fail gracefully. The app should explain what will happen when the connection returns, and never discard what the user has already done.
Subscriptions are a good example. Purchase and restore flows need the store, but entitlement can be cached on the device, so a paying user keeps full access on a flight.
How we test it
Offline behaviour has to be tested on purpose, because it never shows up on a developer’s desk:
- Airplane mode in the middle of every critical flow.
- Slow and flaky network profiles, not just on or off.
- Two devices editing the same data while one is offline.
- Killing the app mid-sync, then reopening it.
- Unit tests on the sync and conflict rules themselves, where most of the risk lives.
Offline-first takes more design up front. In return you get an app that feels fast everywhere, survives bad connections and never loses a user’s work, which is what people remember about an app long after they forget its features.