DEV Community

PRS CloudTech
PRS CloudTech

Posted on

Why field apps need offline-first storage, not a better network

Every few months a team asks us to "fix the sync" on an app that works perfectly in the
office and falls apart in the field. The request is usually framed as a network problem.
It almost never is. It is an architecture problem, and no amount of retry logic fixes it.

The failure most teams ship by accident

The default way to build a mobile app is to treat the server as the source of truth. The
user taps something, the app calls an API, the API answers, the screen updates. This is
clean, easy to reason about, and works flawlessly on office wifi.

Then you give it to someone collecting data in a basement, a warehouse, a rural clinic or
a moving vehicle. The request hangs. The spinner spins. The user taps again. Now you have
two half-finished writes and a user who does not trust the app.

Teams respond by adding retries, then a longer timeout, then a queue bolted onto the
network layer. Each patch makes the code harder to follow and none addresses the real
issue: the app cannot function without a round trip, so every weak signal is an outage.

What offline-first actually means

Offline-first inverts the relationship. The device's local database is the source of truth
for the user interface. A tap writes locally and returns immediately. Sync is a separate,
background concern that reconciles local state with the server whenever a connection
happens to exist.

The practical consequences:

  • The UI never waits on the network. Reads and writes hit local storage, so the app is as fast in a dead zone as on wifi.
  • Connectivity becomes a status, not an error. "3 items pending" is information. A failed request with a red toast is an interruption.
  • The user's work is never lost. It was written to disk before anything was attempted over the wire.

Where it gets genuinely hard

Anyone can cache reads. The difficulty is writes, and specifically conflicts.

You need stable IDs generated on the device. If the server assigns the primary key, the
device cannot reference a record it just created until the server has seen it. Generate a
UUID client-side and let the server accept it.

You need every write to be idempotent. The device will retry. If a retry creates a
second record, you have corrupted data rather than a slow app. The device-generated ID is
what makes a retry safe — the server can recognise a record it already has.

You need a conflict rule decided by the business, not the code. Two people edited the
same record offline. Last-write-wins is the common default and is frequently wrong: it
silently discards someone's work. For an inventory count, you may want both values
preserved and flagged. For a patient note, you almost certainly want an append-only log
rather than an overwrite. This is a product decision, and if nobody makes it explicitly,
the framework makes it for you — badly.

You need a real queue, with ordering. If a record is created and then updated while
offline, those operations must reach the server in order. An unordered queue will try to
update a record the server has never seen.

The part that bites later

Schema changes. A device that has been offline for three weeks is running the old schema
and holds unsynced writes. If your migration assumes a clean local database, that user's
pending work is gone on first launch after the update.

This means local migrations need the same discipline as server migrations — versioned,
forward-only, and tested against a database that contains unsynced rows. It is tedious and
it is the difference between an app that survives a year in the field and one that quietly
loses data.

When not to do this

Offline-first is real engineering effort and it is not always warranted. If your app is
used exclusively on reliable connections, or every action inherently requires the server
anyway — a payment authorisation, a live price check, a seat booking — you are adding
complexity for no benefit. An offline payment confirmation is not a feature, it is a lie
to the user.

The question worth asking is not "could this work offline" but "what does the user lose if
this specific action fails right now". If the answer is "ten minutes of data entry", build
offline-first. If it is "they try again in a second", do not.

A reasonable default

For apps where field use is real, we build with a local database as the primary store,
device-generated UUIDs, an ordered and idempotent outbound queue, an explicit conflict rule
agreed with the client, and versioned local migrations. In Flutter this is well-trodden
ground, and the cost is front-loaded into the data layer rather than spread across every
screen as retry handling.

The test is simple and worth running before launch: put the device in airplane mode, use the
app normally for ten minutes, reconnect, and check that nothing was lost, nothing was
duplicated, and the user was never blocked. Most apps fail this test. The ones that pass it
are the ones nobody complains about.


PRS CloudTech builds mobile apps, websites, ERP, CRM and
e-commerce platforms. Founded 2016, offices in New Delhi, Noida and Brisbane.

Top comments (0)