Writing

How do you build an offline-first app that never loses a record?

How a field app keeps working with no signal, and why the phone, not the server, has to be the source of truth until every record is confirmed.

A field crew fills in an inspection form at a site with no mobile coverage. They tap Save. What happens next decides whether the business trusts the app.

Most apps treat "offline" as an error state: a spinner, a retry button, a message asking the user to try again later. For people whose whole working day happens out of coverage, that is the same as not having an app. The requirement for the field operations app I built was stricter: the app must work exactly the same with no signal as with full signal, and nothing a crew enters may ever be lost.

The phone is the source of truth

The first decision is the one everything else follows from. Until the server has confirmed a record, the copy on the phone is the only real copy. That sounds obvious, but it inverts the usual mental model, where the server is the truth and the app is a window onto it.

In practice it means every record is written to local storage on the device first, synchronously, before the user sees "Saved". The upload is a separate job that runs later. The user never waits for the network, because the network is not part of saving.

A queue, not a request

Each saved record goes into an outbox: an ordered queue on the device. A background worker drains the queue whenever a connection exists, oldest first, one record at a time. If an upload fails, the record stays in the queue and the worker backs off and tries again. If the app is killed, the queue is still there on the next launch.

The crucial rule: a record leaves the queue only when the server replies with an acknowledgement that includes the record's own identifier. A network timeout is not a failure and not a success; it is unknown, and unknown means the record stays.

Identifiers are made on the device

If the server assigns identifiers, the phone cannot know whether a timed-out upload actually arrived, and a retry creates a duplicate. So every record gets a unique identifier the moment it is created, on the device. The server treats uploads as idempotent: the same identifier twice is the same record, not two. Retrying becomes safe, and safe retries are what make "never lose a record" achievable at all.

Photos are records too

Photos are the part people forget. They are large, they fail more often, and a form with three photos is not complete until all three arrive. Each photo is its own queue item, linked to its form by identifier, uploaded after the form's data so the office sees the record immediately and the images fill in behind it. The form shows as "syncing" until the last photo is confirmed, so nobody mistakes a partial upload for a finished one.

Show the state, honestly

The app's sync screen lists every queued record with its status: queued, uploading, synced. A badge on the tab bar shows how many are still waiting. When a crew gets back into coverage they can watch the queue drain, and when it reads zero they know, rather than hope, that the day's work is in.

This visibility turned out to matter as much as the engineering. Trust in an offline app comes from being able to see that it kept its promise.

What it changed

Data entry became 60% faster, because it happens once, on site, instead of once on paper and again in the office. Sync errors dropped to near zero, because nothing is retyped and nothing is dropped. And the office stopped asking crews to "send that through again".

The pattern is not complicated. It is a short list of rules, applied without exception: write locally first, queue uploads, make identifiers on the device, treat the server's acknowledgement as the only definition of done, and show the user the truth about what has and has not synced. This is the approach behind the field operations app.

Common questions

What does offline-first mean for a mobile app?

The app saves everything on the device first and works identically with or without a connection. Uploading to the server is a separate background job, so the user never waits on the network and nothing is lost when there is no signal.

How do you stop duplicate records when an upload is retried?

Create each record's unique identifier on the device and make the server idempotent, so receiving the same identifier twice updates one record instead of creating two. That makes retries safe.

Which technologies work well for offline-first apps?

Flutter with local storage on the device, a small API on Cloudflare Workers, and Supabase for the database is a dependable combination. The pattern matters more than the stack: local write first, a queue, device-made identifiers, and acknowledgement as the definition of done.

How long does an offline-first app take to build?

It depends on the forms and workflows involved. The offline and sync layer itself is a well-understood piece of work once the rules above are followed; most of the effort goes into the business logic around it. Discovery settles the scope and the timeline.

Found this useful? Share it: LinkedIn · X ·

Keep reading

Working on something like this? Tell me about it and I'll reply with questions, not a quote.