What is an offline-first app?
An offline-first app reads from and writes to a database on the device first, and treats the network as something it synchronises with when it can. Android's guide to building offline-first apps defines it as an app that can perform all, or a critical subset, of its core functionality without internet access. In that design the local data source is the canonical source of truth for everything the screens show, and the user interface never talks to the network directly; a repository layer fetches from the server and updates the local store. The minimum is that reads work without a connection.
That is a different thing from caching a few screens. A cached screen shows yesterday's data and fails the moment the user tries to change something. An offline-first app lets a technician in a basement, a warehouse worker behind steel shelving or an inspector on a rural site finish their work, and it tells them honestly which parts have reached the server. The real design work is not the local database; it is deciding which actions may happen offline, how queued changes reach the server exactly once, and what happens when two people changed the same record.
Which actions should work offline?
Decide action by action, because some writes must never happen without the server. Android's guide describes three write strategies: online-only writes for time-sensitive operations such as bank transfers, queued writes for non-critical data such as analytics, and lazy writes, where the app writes locally first and sends the change at the earliest opportunity. Lazy writes are what make an app offline-first, and they are the ones that need conflict resolution. A scoping table for a field-service app might look like this:
| Action | Offline behaviour | Reason |
|---|---|---|
| View today's jobs, addresses, equipment history | Read from the local database, show last sync time | The worker must start without signal |
| Record notes, photos, parts used, customer signature | Lazy write: save locally, queue for sync | This is the work itself; losing it is the main risk |
| Usage analytics and diagnostic logs | Queued write; may be batched or dropped when old | Useful but not critical |
| Take a payment, allocate stock from a shared pool | Online only; disable with a clear message | Two devices could promise the same item or charge twice |
| First login on a new device | Online only | Identity and permissions must be confirmed by the server |
For each offline action, also write down which data may stay on the device, for how long, and what happens after logout or when a user's access is revoked.
How should offline writes be queued and synchronised?
Store every change together with an entry in a durable operation queue, in the same local database transaction, and send the queue in order when a connection returns. Each queue entry needs:
- An operation ID generated on the device, so the server can recognise a retry and apply it only once.
- The record ID, also generated on the device for new records, so the app never waits for the server to name something the user just created.
- The base version of the record the user edited, so the server can detect that someone else changed it in the meantime.
- The payload and creation time, plus any dependency: a photo cannot upload before the visit it belongs to exists on the server.
The upload needs to survive the app closing or the phone restarting. On Android, WorkManager is built for exactly this: scheduled work is stored in an internal database and rescheduled across reboots, it can wait for a constraint such as a connected network, and failed work can retry with exponential backoff. On every platform, also run a sync when the app returns to the foreground instead of relying only on background scheduling.
The other direction matters as much. Android's guide contrasts pull-based sync, where the app fetches data on demand, with push-based sync, where the device keeps a replica of the data it needs and the server signals what has changed. Push-based sync lets an app stay offline for long periods but requires versioning. Whichever you choose, make deletions explicit: if the server simply stops returning a cancelled job, a device that was offline will keep showing it. Return deletion markers, sometimes called tombstones, in the change feed.
How do you resolve conflicts between devices?
Detect conflicts with record versions, then resolve them field by field according to what each field means. "Last write wins" is the simplest rule; Android's guide describes it with timestamps attached by devices, where the server discards data older than its current state. It is acceptable for a bookmark. It is dangerous for business records, because device clocks drift and because it silently throws away one person's work.
AWS AppSync's conflict documentation shows the main alternatives concretely. With optimistic concurrency, the server rejects a write whose version differs from the current one and returns the latest item; the client merges and retries. With Automerge, a conflicting scalar field keeps the server value, a conflicting list has the incoming values appended (duplicates retained), a set receives the union of both, and new fields are merged in. A custom handler can instead resolve, reject or remove the item according to your own rules. These rules are a useful vocabulary even if you never use AppSync, because they force a decision for each kind of field.
Write that decision down as a policy table. A hypothetical service-visit record could use:
| Field | Rule on conflict |
|---|---|
| Technician notes | Append both; notes are a list, nothing is lost |
| Parts used | Merge both sets, then flag quantities for review |
| Appointment time | Server wins; dispatch owns the schedule, and the technician is told |
| Customer signature and photos | Never overwrite; keep every submitted item as evidence |
| Visit status | "Completed" against "rescheduled" goes to a person to decide |
| Approved invoice amount | Reject offline edits once the amount is approved |
Keep both versions and the resolution in an audit record. When a conflict needs a person, show the user which fields synced and which are waiting for a decision.
Hypothetical example: a visit recorded in a basement
A technician syncs at 09:10 and reaches a plant room with no signal at 10:30. At 10:40 a dispatcher moves the visit to the next day, so the server version goes from 7 to 8. At 10:55 the technician, still on version 7, records a replaced valve, adds a note, captures the customer's signature and marks the visit completed. The app shows each change as "saved on this device, waiting to sync".
At 11:20 the phone reconnects and the queue sends four operations in order. The server sees base version 7 against current version 8 and applies the policy: the note is appended, the valve is added to parts used, the signature is stored as evidence, the appointment time stays as dispatch set it, and the status conflict ("completed" against "rescheduled") is flagged. The technician sees a banner saying the visit needs a dispatcher's decision; the dispatcher sees both versions side by side. Stock and billing are calculated on the server only after the status is settled. Nothing the technician entered is lost, and nothing is silently overwritten.
How do you keep offline data on the device secure?
Store only what the offline work needs, protect it at rest and plan how it is removed. OWASP's mobile application security standard notes that apps routinely store personal data, cryptographic material, secrets and API keys locally, and that such data can also leak unintentionally through system features such as backups and logs; its first storage control is simply that the app stores sensitive data securely. In practice, sync today's and tomorrow's jobs rather than the whole customer base, encrypt the local database, keep keys out of plain files, decide whether the data may appear in device backups, and keep personal data out of logs. Set a maximum offline period after which the user must sign in again, and wipe local data when an account is revoked. If a sign-in token expires while changes are queued, keep the queue, ask the user to sign in, then continue; never discard unsynced work on logout without warning.
Does offline-first work for web apps?
Yes, with the same data rules and different plumbing. Jake Archibald's offline cookbook describes the service worker strategies: serve the application shell from the cache, use stale-while-revalidate for content that may be a little old, and be careful with "network falling back to cache", because on a slow or intermittent connection the user waits for the network to fail before seeing content already on the device. Background sync can send non-urgent updates once connectivity returns. The response cache is not a place for records the user edits; those belong in a local database with the same operation queue, versions and conflict rules as a native app. Check support for background features on the browsers your users actually have, and sync whenever the page is open as a fallback.
How should an offline-first app be tested?
Test transitions, not just the two states of "online" and "offline":
- Launch the app with no network after a fresh install and after a normal day's sync.
- Drop the connection halfway through a photo upload, then restore it.
- Stay offline for several days, until the sign-in token has expired.
- Set the device clock wrong by an hour and by a day.
- Edit the same record on two devices and on the back office while both devices are offline.
- Kill the app during sync, and restart the phone with a non-empty queue.
- Install an app update that changes the local database schema while operations are still queued.
- Toggle connectivity repeatedly, as happens in lifts and on trains.
Measure the age of the oldest pending operation, sync success rate, conflicts per hundred operations, duplicates rejected by the server and time to fully recover after reconnecting. The target for lost user entries is zero; any loss should be treated as an incident.
Common mistakes
- Showing "saved" when the change exists only on one device.
- Resolving every conflict with the device clock.
- Letting only the server create IDs, so records made offline cannot be referenced.
- Clearing the queue on logout or token expiry.
- Shipping without a migration plan for the local database.
Where should you start?
Start with the one workflow users must finish without signal, fill in the action table and the conflict policy for it, and only then choose a sync library or service. Offline operation is part of the scope on our mobile app development service page, where you can also book a free discovery call.