Shipping offline-first on iOS without losing your mind
Notes from building a waste-management app that has to work in basements, tunnels, and remote depots. Hard-won lessons about CRDTs, conflict resolution, and the design language of 'sync'.
Notes from building a waste-management app that has to work in basements, tunnels, and remote depots. Hard-won lessons about CRDTs, conflict resolution, and the design language of 'sync'.
Most apps say they “work offline”. Most apps are bending the truth. They work without a network round-trip on the happy path, which is a different thing.
When we set out to build WTN App, the brief was sharper: the app must let a driver pick up a load on the side of an A-road, sign for it, hand the phone to a producer who has never heard of the app, get their signature, and then drive into a tunnel for ten minutes. And the next leg of the chain has to be able to pick the load up cleanly six hours later, perhaps from a completely different phone with no shared network history.
This is the kind of offline that breaks libraries.
There is reconciliation. “Sync” implies two states converging on one truth. What you really have is a graph of edits, each with timestamps you do not quite trust, each made by an actor who did not have the full context. The job of the server is not to “sync”. It is to be the place where every party’s view of the chain meets, and to refuse to let one party silently overwrite another.
We landed on a hybrid: append-only event stream per chain, plus a server-side projection that resolves conflicts deterministically based on the role of the editor. Producers can edit producer fields. Carriers can edit carrier fields. Nobody can retroactively re-sign for someone else. It is not glamorous. It works.
The trickiest moment in the chain is when the producer hands the phone to the carrier. Both parties might be offline. The app needs to mark the producer’s signature as final, lock the producer-editable fields, and prepare a token the carrier’s app can ingest later.
We do this with a short-lived QR code that encodes the chain identifier plus an HMAC of the producer’s snapshot. The carrier scans the QR, takes their step, signs, and the chain enters the next state. When either device gets signal, the same event stream reconciles up to the server and onward to the consignee.
The most useful design decision was building a small, honest “sync state” badge in the corner of every screen. Three states, three colours:
Drivers learned the badge in five minutes. We hear more about it in support conversations than any other piece of UI.
We will write the long version of this someday. For now, this is the part that comes up in every conversation.