B2B Distribution · Next.js + PWA
A field tool that holds together when the signal disappears.
Drivers were running routes from paper. Dispatchers were writing phone orders in the margins of those manifests because the wholesale portal couldn't catch them. Any app that broke the second a truck hit a rural zone — or that ignored the phone-order workflow — would have been abandoned by the end of the first week. We built one that survived both.
In one sentence
A Next.js + Tailwind PWA that runs entirely offline in the field, syncs idempotently on reconnect, and bakes the phone-order intake form into the same UI drivers use — no paper margins, no end-of-shift reconciliation, no app abandoned the moment signal drops.
The Problem
Paper was winning because nothing else could survive the field.
A B2B distributor moved most of its weekly volume through two motions the wholesale portal could not represent. Dispatchers took orders by phone and wrote them into the margins of printed route sheets. Drivers ran the routes on those same sheets, hand-writing returns and substitutions as they went. Reconciliation happened at the end of every shift — from memory, paper, and best guesses.
The team had tried connected tools before. Each one made the same fatal assumption: that a tablet on a delivery truck would have signal when it needed it. The moment a driver hit a rural pocket or a steel-roofed loading dock, the app failed silently — and the driver went back to paper. Every retry burned a little more of the team's willingness to adopt anything new.
She would not learn a tool that forgot her phone orders.
— The dispatcher, repeated across discovery calls
That became the design constraint, not a feature request. The ops lead pushed harder — before any UI was approved, the team wanted proof that sync replay was idempotent. They had been burned by half-baked offline tools before and they were not going to be burned again.
This is what the engagement had to solve, in priority order:
- Every screen — load, capture, navigate, reconcile — has to work with zero connectivity. Not degraded. Not warning-banner-yellow. Working.
- Phone-order intake has to be faster than paper. If a dispatcher feels even a second of friction over the pen-and-clipboard flow, the tool loses.
- One sync on reconnect. No partial state. No duplicate orders. No silent drops. Reconciliation can never produce something a human has to chase down.
Reconciliation alone was eating roughly an hour a day per dispatcher. That isn't a productivity nicety. Bad sync means lost orders, and lost orders mean a customer calling angry the next morning. The cost of getting this wrong was trust — the kind that takes years to rebuild on a distribution route.
The Approach
Start from the offline contract. Draw the screens last.
Most teams would build the connected web app first and patch in offline support later. We inverted it. The state machine, the sync queue invariants, and the conflict-resolution rules were locked before a single screen was sketched — because every screen decision had to live downstream of those invariants. The UI got to be opinionated only after the data layer told it what was provably safe. Five decisions fell out of that ordering:
Key insight
Design the offline contract before the UI. The state machine and sync invariants must be decided before a single screen is drawn — otherwise you'll rebuild the screens.
The state machine was the spec
Each route moves through pinned → in-progress → completed → reconciled. Transitions are append-only locally, idempotent on sync. A driver who re-syncs the same stop twice — because the truck cab radio kicked the network — cannot create a duplicate record. We wrote this down, walked the ops lead through it, and only then started on screens.
Service-Worker-first, not service-worker-eventually
The PWA shell, route data, and stop sequence cache on first install. Subsequent loads are entirely offline; the network is opportunistic, never blocking. We chose this over a thin online-first wrapper because every previous tool the team had tried failed on the same line: it treated offline as an edge case instead of the default.
Dispatcher intake is the same screen as driver intake
Phone-order capture is a first-class screen in the PWA, not a separate admin module. Dispatchers enter customer, line items, and delivery date in the same UI drivers see. One data model, one source of truth — no more two-system reconciliation, no more orders living in the margin of a manifest.
Conflict resolution is explicit, not implicit
On reconnect, the server reconciles local mutations into a canonical route log with rules written down in plain language: driver mutations win for in-field state; dispatcher mutations win for order intake. When the rules disagree, the system surfaces a banner — it does not silently pick a winner. Implicit conflict resolution is how you lose trust in week three.
17 screens, one design system, hours per screen
The Tailwind palette and the four base components — Card, Field, ListRow, Stepper — were locked at the system layer before screen work began. New screens compose from existing primitives. Adding screen 18 is a Tailwind exercise, not a re-architecture.
Implementation
What we built, and why we built it that way.
Next.js 15 App Router — Server Components by default
Only the screens that capture mutations are Client Components. Everything else renders on the server, which keeps the JS payload small enough to install over a weak cell connection — a real constraint when the install happens once, in the field, and may never see strong Wi-Fi again. Route data is fetched via Server Actions and cached locally on first load.
Service Worker + IndexedDB — the sync queue is the product
A Workbox-shaped service worker handles app-shell caching. Mutations queue into IndexedDB and replay on reconnect. The replay queue is idempotent at every layer — a partial replay can resume cleanly without operator intervention, which is the only acceptable behavior when the operator is a driver halfway down a country road. We chose IndexedDB over LocalStorage for one reason: structured, queryable mutation history that survives a tab crash.
Tailwind v4 — locked palette, four primitives, zero drift
Brand palette declared once in @theme inside globals.css. No inline styles. No custom hex outside the palette. Four base components compose into all 17 screens. We did this on purpose: a locked system is the only way a 17-screen surface area stays consistent under speed, and consistency is what a field user notices the moment they pick up the tablet at 6 a.m.
Claude Code drove the surface, humans drove the invariants
Claude Code scaffolded the 17 screens, the Tailwind class composition, and the accessibility wiring — the parts where pattern-completion is a strength. Hand-coded: the state machine, the conflict-resolution rules, the sync-queue invariants. AI pattern-completion gets state machines subtly wrong in ways that surface only at scale, on the truck, on a Friday afternoon, when nobody is watching. Those rules were written by a human, reviewed by a human, and tested by a human.
Results
What changed in the field.
Drivers run full shifts in zero-bar zones
Every screen loads, captures, and reconciles offline. Trucks that used to fall back to paper at the first rural pocket now finish the route on-device.
Phone-order intake collapsed to a 35-second flow
Before: a dispatcher wrote the order on paper, typed it into the wholesale portal later, and reconciled from memory. After: same intake form drivers use, no second system, no manual reconciliation.
Sync replay is idempotent across 100% of tested mutation patterns
Every conflict scenario we could write down — duplicate stop completions, partial-queue replay after a tab crash, returns posted twice — reconciles cleanly on reconnect. No silent drops, no operator chasing ghosts.
End-of-shift reconciliation went from an hour to a banner
Dispatchers stopped reconciling from paper margins. The only manual touch left is the rare flagged conflict, surfaced explicitly in the UI.
New screens land in hours, not days
A locked palette plus four base components means screen 18 — and 19, and 20 — is a composition exercise, not a redesign. The cost of asking "can we add one more thing" collapsed.
Takeaway
If you take one thing from this case study even if you never hire us: design the offline contract before the UI. Every screen decision lives downstream of the state machine and the conflict-resolution rules. If you patch offline support in later, you are not patching — you are rebuilding the screens, the data model, and the trust of the team that has to live with the result.
Public artifacts
Try the demo. Or ask for the architecture.
Open the PWA — driver, dispatcher, HQ
17 screens, fully synthetic data, all three roles wired (driver app, dispatcher intake, HQ fleet dashboard). Try a route, capture a stop, simulate the phone-order intake. Add ?review=1 to the URL for the in-wireframe annotation mode.
Architecture deck on request
The implementation repo is private. We share the architecture deck — state machine diagrams, sync queue invariants, conflict-resolution rules — on request once we've talked through your context.
Your field workflow has connectivity gaps it can't admit.
Let's design the offline contract before you commit to a stack.
30 minutes. We'll talk through your worst-case sync scenario — the rural route, the dropped order, the dispatcher who refuses to adopt — and tell you whether the architecture you're considering can survive it.
Book a 30-min call