Every warehouse has a dead spot. Ours is halfway down the high racking, where steel shelving and a few tonnes of stock do to Wi-Fi what they do to everything else. A picking app that needs the network for every tap dies right there, holding your pick list.
Taps are local first
On the iPad picker, a tap never waits for the server. The row flips to picked on the same frame, written to the local store; the network call is a side effect. Pending mutations sit in a queue on the device, and a network monitor watches for the online edge — the moment connectivity comes back, the queue drains oldest-first. The server endpoint is idempotent, so a tap replayed twice can't pick twice.
Failures back off two seconds, doubling to a cap, and retry for as long as it takes — a transport failure never expires a pick. Only an action the server actually refuses — a 4xx that would refuse again tomorrow — is parked as dead-lettered rather than blocking everything behind it, visible on the iPad's diagnostics screen where support can clear it. The toolbar pill tells the picker the truth the whole time.
# action state tries
1 pick A1 · ×2 sent 1
2 pick B3 · ×1 pending 0
3 qty C2 · 1→3 pending 0
× pick D4 · ×1 dead 10 ← parked; clear via DiagnosticsThe bug our own audit caught
Offline queues have a classic failure mode: a refresh from the server arrives while taps are still queued, and the server's older view clobbers the picker's newer one. Our reliability audit caught exactly that — a drain pass could erase taps queued mid-sync. The rule now is simple: any item with a pending queued action keeps its local state on refresh, and the pending flag clears only after a successful drain. The server doesn't get to overwrite a tap it hasn't seen yet.
You can't pick what's already packed
While someone picks, the bench packs. The pack–pick interlock locks an order's units at picked the moment a label exists for them — the count can't drop below what's already in a box. Void the label and the lock releases on the next refresh. There's no separate lock table to corrupt; it's computed live from active shipments.
The honest limits: offline-first is not offline-forever. A batch has to be opened online at least once to fill the cache, and creating a new batch needs the server to resolve its filters against the orders table. Scanning, which we originally deferred, has since shipped — camera or hardware scanner, off by default per organisation — and it rides the same offline queue as a tap.