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.