HackSec Infotech

Mobile

Offline-first is not a feature, it is an architecture

What we learned building a patient app for clinics where the connection drops every few minutes — and why sync conflicts are a product decision.

We built a patient companion app for a network of forty clinics. During field research we sat in six waiting rooms and watched the connection drop, on average, every ninety seconds. That single observation changed the entire architecture — and taught us that “offline support” is not a feature you add. It is a decision about where truth lives.

The question that decides everything

Not “does it work offline?” but “when the device and the server disagree, who is right?” Answer that per data type, early, with the people who own the domain — because it is a product decision wearing an engineering costume.

In our case the answers differed by entity:

  • Appointments: server wins. The clinic's schedule is authoritative; a device cannot create a slot that does not exist.
  • Medication logs: device wins. The patient knows whether they took the tablet; the server has no better information.
  • Profile details: last write wins, with a visible conflict prompt when both sides changed since the last sync.

Writing that table took an afternoon. It prevented months of ambiguity.

Local database as the only source the UI reads

The architectural rule that made everything else simple: the UI never reads from the network. It reads from the local database. A background sync engine is the only thing that talks to the server, and it writes into that same local database.

This inverts the usual loading-state problem. There is no spinner waiting for a request, because there is no request in the render path. Screens are instant whether or not there is a signal, and connectivity becomes a status indicator rather than a blocking condition.

Queue writes, and make them idempotent

Every user action that changes data becomes a queued mutation with a client-generated ID, persisted locally before the UI acknowledges it. The sync engine drains the queue when connectivity allows, with exponential backoff.

The client-generated ID matters more than it looks. Networks fail in the worst possible way: the request arrives, the server processes it, the response is lost. Without an idempotency key the client retries and the patient has now logged the same dose twice. With one, the server recognises the repeat and returns the original result.

Make sync state visible, not silent

Users tolerate delay. They do not tolerate uncertainty. Our sync indicator has four explicit states — up to date, syncing, waiting for connection, and needs attention — and every queued item shows its own state in context.

The most common support complaint in offline apps is not lost data. It is users who cannot tell whether their data was lost.

SMS as the honest fallback

Some patients had phones that could not reliably hold a data connection at all. Appointment reminders therefore fall back to SMS, sent server-side, with no dependency on the app. It is not elegant and it does not demo well. It also raised attendance more than any in-app feature we built.

What we would do differently

We under-invested in sync observability at first. When a device fell behind, we had no way to see it without asking the user to send logs. Adding a lightweight, privacy-respecting sync telemetry channel — queue depth, last successful sync, failure category, no content — paid for itself within a week of launch.

Offline-first is more work than a conventional client-server app. On a 3G connection in a rural waiting room, it is the only thing that works at all.

Need this done, not just read?

We do this work for a living.

If any of the above sounds like a problem you have, a thirty-minute call with the engineer who wrote it is free.

  • Free 30-minute discovery call
  • NDA signed before scoping
  • You own all code and accounts