Offline-first is a product decision, not a caching strategy
Designing for weak connectivity changes what the product promises — so it has to start with the product, not the service worker.
2 min read
When a team says an app "works offline", they usually mean it caches screens. The user can open the app in a tunnel and see yesterday's data. That is useful, but it is not what field workers need. They need to do things offline — record a deposit, register a patient, confirm a delivery — and trust that the work will count.
That shift, from reading offline to writing offline, is a product decision before it is an engineering one.
Decide what the product promises
Every action in an offline-capable app needs an honest answer to one question: what does the user believe happened when they tapped the button?
For a field agent collecting a savings deposit, the answer we designed was: "the deposit is recorded on this phone, a receipt is issued, and it will reach the ledger when the phone next connects." That promise shaped everything else. The receipt says recorded, not confirmed. The member sees a pending state until sync. The agent's end-of-day screen separates what is synced from what is not.
If you do not decide the promise explicitly, the interface will imply one — usually a stronger one than the system can keep.
Every write is a command with an identity
Offline writes cannot be plain API calls, because they may be sent late, sent twice, or sent after the server state has moved on. We model each one as a command with its own id, created on the device:
type Command = {
id: string; // generated on device, used as the idempotency key
type: "deposit.record";
payload: { memberId: string; amountXaf: number };
recordedAt: string; // device time, for the audit trail
};
async function flush(queue: Command[]) {
for (const command of queue) {
const res = await api.post("/commands", command); // server dedupes on id
if (res.ok || res.status === 409) await markSynced(command.id);
else break; // keep order; retry later
}
}
The server treats the command id as an idempotency key. Sending the same deposit twice produces one deposit. That single rule removes a whole category of reconciliation problems.
Conflicts are business rules
Two agents update the same member's phone number offline. Which one wins? "Last write wins" is a technical answer to a business question, and it is often wrong. We list the entities that can be edited offline and, with the client, decide a rule for each: append-only for money, field-level merge for profiles, server-authoritative for anything regulated.
Show sync as a first-class state
Finally, the interface should make the invisible visible: a quiet indicator of what is waiting to sync, a clear distinction between recorded and confirmed, and a way to see what failed and why. Users forgive delays. They do not forgive work that silently disappears.