Threat modelling before the first line of code
Most breaches trace back to a design decision, not a coding mistake. Here is the two-hour session we run at the start of every build — and the template we use.
Mobile
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.
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:
Writing that table took an afternoon. It prevented months of ambiguity.
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.
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.
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.
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.
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.
Keep reading
Most breaches trace back to a design decision, not a coding mistake. Here is the two-hour session we run at the start of every build — and the template we use.
We rebuilt six sites this year. The same four fixes accounted for most of the gain every time — and image budgets were not the biggest one.
Demos are easy. Evaluation harnesses, grounding, injection defence and cost ceilings are what separate a prototype from a product.
Need this done, not just read?
If any of the above sounds like a problem you have, a thirty-minute call with the engineer who wrote it is free.