In short
How do you build a field data capture app that works offline?
Persist every record locally the moment it is entered, before any network request. Queue uploads and trickle them out over whatever connection exists, compressed, retrying without duplicating. Timestamp writes so that independent devices can be ordered when they reconnect, hash them so tampering is visible, and surface conflicting versions of a record to a person rather than letting the last write win. Show the user which records have not yet left the device.
Where the connection actually is
Field systems are specified in offices and used in places where the network is a rumour. A clinic reporting over an intermittent 2G link. A border post whose connection to the primary database runs over a satellite link that is not always there. In both, the application is not waiting for a slow network. It is waiting for one that may not come back for hours.
The failure that matters is quiet. A form is filled in, the request times out, and the record is gone. Nobody upstream sees a gap, because a gap and a quiet district look identical. An officer at a border either has an answer about the person in front of them or does not, and a blank screen is not an answer.
Local first, then the network
The first rule is that entry never waits on a round trip. Records persist to local storage in the browser or on the device the moment they are entered. Closing the tab does not lose the morning. The sync layer is a separate concern that runs behind the interface rather than in front of it.
This changes what the user sees. An unsent record has to be visible as unsent, at the point of entry, so the person who typed it knows it has not left the device yet. A system that shows a green tick on local save and says nothing about the upload is lying by omission.
Store and forward, compressed
Uploads go through a queue. The queue trickles payloads out over whatever connection exists, retries without duplicating, and does not drop fields on a partial send. On a metered 2G link the size of a submission is a cost somebody is paying, so payloads are compressed for the connection rather than for the server.
The flush has to stay off the main thread. During a high-volume event a backlog of hundreds of records can build up, and an interface that locks while clearing it is an interface that gets abandoned at exactly the moment the data matters most.
Design the reconciliation, not the cache
Offline first is usually sold as a caching problem and it is not. Several devices that have been operating independently will come back online holding versions of the same record that disagree, and something has to decide between them.
Last write wins is the default and it is the wrong answer for anything that will be relied on later. Every local write should carry a vector clock, so that conflicting records can be ordered rather than guessed at, and a hash, so that any change between write and sync is evident rather than silent. Reconciliation then happens against the primary database when connectivity returns, with the conflicts surfaced to a person instead of resolved by whichever device happened to reach the server first.
Anything that resolves conflicts quietly is a system that will one day be confidently wrong, and nobody will be able to say when.
The log has to hold up later
On surveillance and verification work the audit log is not an operational nicety. It is the record that has to survive a dispute months afterwards, which means it has to be tamper-evident by construction rather than by policy. Hashing each write at the moment it is made gives you that without a separate system.
We built both halves of this on two engagements, one through GIZ for a police information system at land border crossings and one through AMREF Health Africa for a DHIS2 field data entry interface in remote clinics. Neither permits us to publish operational detail, and the mechanism above is all that is drawn from them.
Questions
Is offline-first the same as caching?
No. Caching keeps a copy of what the server said so the app can read it without a connection. Offline-first means the app can also write without a connection, and that is where the difficulty lives, because writes made on different devices have to be reconciled when they meet.
Why not let the last write win?
Because it resolves a conflict without anybody knowing there was one. For a field record that will be relied on later, a wrong answer that nobody flagged is worse than a conflict that a person had to resolve. Order writes with vector clocks and surface the disagreement.
What should the user see while a record is unsent?
That it is unsent, at the record itself. Local persistence protects the data, but the person who entered it needs to know it has not reached the server, or they will assume it has and move on.
Does this work in a browser, or does it need a native app?
A browser is enough for the clinic case. Records persist to IndexedDB, the queue runs in the page, and the interface stays usable with the network gone. A native app buys background sync and more storage, which matters for some workloads and not for most.