Skip to main content
WZ SolutionsWZ SolutionsSoftware Engineering
Architecture

Designing offline-first field applications that actually reconcile

Caching data on the device is the easy part. The hard part is deciding what happens when two people edited the same record in the same afternoon.

WZ Engineering2 min read

Field applications in our market have to assume there is no network. Meter readers walk routes without coverage, technicians work inside buildings that block signal, and agents travel between districts where connectivity comes and goes. An application that degrades gracefully is not enough — it has to work completely offline and then reconcile without losing anything.

Local-first, not cache-first

The distinction matters. A cache-first app treats the server as authoritative and the device as a temporary copy; when the two disagree, the device usually loses. A local-first app treats the device write as a real event with its own identity and timestamp, queued for synchronisation. The user never waits for a network round-trip to know their work was saved.

  • Every write is an append to a local operation log, not an in-place mutation
  • Each operation carries a client-generated UUID so retries are idempotent
  • The UI reads from local state exclusively — sync status is a separate, visible signal
  • Sync is a background process that can fail and retry without blocking work

Conflict rules belong to the business, not the code

The most common mistake is inventing conflict resolution in the sync layer. Whether a supervisor's status change should beat a technician's is a business decision with operational consequences. We take those rules to the operations team, write them down, and implement exactly what was agreed.

In practice a small taxonomy covers most cases: descriptive fields take last-write-wins, status transitions are server-authoritative and validated against a state machine, and numeric quantities that represent physical counts are never merged — they raise an exception for a human to resolve.

type Resolution = "last-write-wins" | "server-authoritative" | "manual-review";

const conflictPolicy: Record<string, Resolution> = {
  "job.notes": "last-write-wins",
  "job.status": "server-authoritative",
  "reading.value": "manual-review",
};

Make sync state visible

Users trust an offline app when they can see what it is doing. A persistent indicator showing pending, syncing and failed counts — with a way to inspect the failures — turns an invisible background process into something an operator can reason about and escalate.

If a field worker cannot tell whether their morning's work has reached the server, they will keep a paper backup. And then you have two systems again.
Keep reading

Related articles

Working on something like this?

If any of the above is a problem you are living with right now, we would be glad to compare notes.

Or email walter.mussagy@gmail.com — we reply within one business day.