Offline-First App Architecture
Engineering

Offline-First App Architecture

Caching a few screens is not offline-first. This guide covers the action table, the sync queue and the conflict policy that let field staff finish their work without losing a single entry.

Short answer

An offline-first app keeps the data users need in a database on the device, lets them read and record work without a connection, and synchronises a durable queue of changes when the network returns. The hard parts are deciding which actions may happen offline, making every queued change apply exactly once on the server, and resolving conflicting edits field by field instead of letting the last write silently win.

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:

ActionOffline behaviourReason
View today's jobs, addresses, equipment historyRead from the local database, show last sync timeThe worker must start without signal
Record notes, photos, parts used, customer signatureLazy write: save locally, queue for syncThis is the work itself; losing it is the main risk
Usage analytics and diagnostic logsQueued write; may be batched or dropped when oldUseful but not critical
Take a payment, allocate stock from a shared poolOnline only; disable with a clear messageTwo devices could promise the same item or charge twice
First login on a new deviceOnline onlyIdentity 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:

FieldRule on conflict
Technician notesAppend both; notes are a list, nothing is lost
Parts usedMerge both sets, then flag quantities for review
Appointment timeServer wins; dispatch owns the schedule, and the technician is told
Customer signature and photosNever overwrite; keep every submitted item as evidence
Visit status"Completed" against "rescheduled" goes to a person to decide
Approved invoice amountReject 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":

  1. Launch the app with no network after a fresh install and after a normal day's sync.
  2. Drop the connection halfway through a photo upload, then restore it.
  3. Stay offline for several days, until the sign-in token has expired.
  4. Set the device clock wrong by an hour and by a day.
  5. Edit the same record on two devices and on the back office while both devices are offline.
  6. Kill the app during sync, and restart the phone with a non-empty queue.
  7. Install an app update that changes the local database schema while operations are still queued.
  8. 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.

Key takeaways

  • Decide per action: reading and recording work go offline, payments and shared stock stay online-only.
  • Save each change and its queue entry in one local transaction, with a device-generated operation ID and base version.
  • Make the server apply each operation ID only once, and send deletions as explicit markers.
  • Write a conflict rule for every field; never let device clocks decide business records.
  • Encrypt local data, limit what is synced, and never drop unsynced work on logout or token expiry.
  • Test transitions: drops mid-upload, days offline, clock skew, two editors and app updates with a full queue.

Frequently asked questions

What is the difference between offline-first and an offline mode?

An offline mode usually means an online app that keeps working for a while when the connection drops, often read-only. Offline-first means the device database is the source the screens read from, writes are saved locally first, and synchronisation happens in the background. The second needs a durable queue and conflict rules from the start.

Which database should an offline-first mobile app use?

Any embedded database with transactions can work; Android's guidance uses Room, for example. What matters more is that each change and its queue entry are written in one transaction, that records carry versions, and that there is a migration plan for app updates.

Is last write wins good enough for conflict resolution?

Only for low-value data where losing one edit is acceptable, such as a display preference. For business records it silently discards someone's work, and device clocks can be wrong. Detect conflicts with versions and choose a rule per field: merge, server wins, keep both or send to a person.

How long can a device stay offline?

As long as your security and business rules allow. Set a maximum offline period after which the user must sign in again, limit the data kept on the device, and design synchronisation so a device returning after days can catch up through a change feed that includes deletions.

Can a web app or PWA be offline-first?

Yes. A service worker serves the application shell and cached content, and background sync can send non-urgent updates later. Records the user edits should still live in a local database with the same operation queue, versions and conflict rules as a native app.

All articles