The first version of the delivery app assumed connectivity and treated its absence as an error state. In the areas the client actually operates, connectivity is the exception for parts of most shifts, and treating it as an error meant the app spent a meaningful fraction of every day in an error state.
Reframing the default
We rebuilt around the opposite assumption: the app works offline by default, and connectivity is a bonus that syncs things faster when it's available. That single reframing changed almost every screen, because a screen designed to show a spinner while waiting for a network call has to become a screen that shows the last known state confidently and syncs quietly in the background.
What we got right early
- Every action queues locally first, whether or not there's a connection, and the interface doesn't distinguish between "sent" and "queued to send" in a way that implies the driver needs to do anything differently.
- Sync is a background process the driver never has to think about, retried automatically with backoff, visible only as a small status indicator rather than a blocking action.
- Photos and signatures are stored locally at full quality and uploaded opportunistically, rather than compressed aggressively up front to survive a bad connection that might not even be there.
What we got wrong first
We initially built conflict resolution around "last write wins" for delivery status, copying a pattern that had worked well on a previous fleet project. It was wrong here specifically because two different people can legitimately update a delivery's status from two different devices — the driver marking it delivered and a depot marking it returned, both true at slightly different times — and last-write-wins silently discarded whichever one happened to sync second.
The fix
- Status changes are appended, not overwritten. The current status is derived from the sequence of events, and any event that seems to contradict a later one is flagged for a human rather than one silently winning.
- The append log itself is the source of truth, not a status field, which sounds like a small distinction and rewired a meaningful part of the data model.
Offline-first is not primarily a networking problem. It's a data modelling problem that happens to be triggered by networking.
What we measured to know it worked
Time from a status change on a driver's phone to it being correctly reflected at the depot, tested specifically under an interrupted connection rather than a clean one, because a clean-connection test tells you almost nothing about whether the offline design actually holds up.
What we'd scope in from day one next time
The append-only event model, rather than arriving at it after the last-write-wins version caused a week of confused depot staff. It's now the default we start from on any offline-capable build, regardless of the specific domain.
