Northvane Logistics
Northvane's dispatch console couldn't scale past 200 fleets. We rebuilt it as a real-time React app on edge, cut onboarding from 9 days to 11 hours, and gave their ops team a console operators actually liked using.
- // sector
- Supply chain · SaaS
- // year
- 2025
- // duration
- 14 weeks · then retainer
- // role
- Lead engineering · design system · platform

// dispatch console · production · q3 2025
// 01 — brief
Northvane runs the dispatch backbone for mid-size logistics carriers across North America. Their original portal was a Rails app from 2016 with a jQuery UI layer bolted on. It worked, until it didn’t — somewhere around 200 fleets the latency between a driver event firing and the dispatcher seeing it crossed five seconds, and onboarding a new operator took the better part of two weeks.
They came to us with a clear ask and an unclear path: the console had to feel instant, the team had to keep shipping during the rebuild, and the existing Rails API had to stay in place. No greenfield rewrite. No down-week.
// 02 — constraints
The real ones, not the polite ones.
- 01Existing Rails API stays — no backend rewrite, no schema changes in flight.
- 02Zero downtime cutover. Operators dispatch live freight; a 30-minute outage cancels routes.
- 03Two senior engineers from Northvane, one PM, no dedicated designer.
- 04SOC2 Type II in flight — every dependency had to clear procurement.
- 05Mobile-first for field ops, but desktop-dense for the dispatch room.
// 03 — outcome
+312%
operator throughput · 6 months post-launch
Measured as dispatched loads per operator-hour. Onboarding time fell from 9 days to 11 hours. P95 event-to-render latency dropped from 5.4s to 184ms.
11h
onboarding (was 9d)
184ms
p95 event→render
0
P1 incidents post-cutover
// 04 — approach
Strangler fig, not big bang.
We didn’t replace the Rails app. We put a new React surface in front of it and migrated screens one by one, behind a feature flag scoped per fleet. Each migrated screen used the existing REST endpoints, plus a thin WebSocket gateway we wrote to push the events the legacy backend was already emitting to its internal queue.
The win wasn’t the framework swap. It was the data model: we modelled the dispatch board as a single source of truth on the client (a normalized store hydrated on connect, then patched by events) so the UI never had to re-fetch to feel current. Everything operators interacted with became optimistic by default, with reconciliation against the server within one tick.
The first time we ran a peak shift on the new console, two dispatchers asked if something was broken because the screen wasn’t lagging anymore.
// 05 — architecture
// edge & runtime
- React 19
- TanStack Start
- Cloudflare Workers
- Durable Objects
// realtime
- WebSocket gateway (Hono)
- Redis Streams
- Server-Sent Events fallback
// data
- Existing Rails REST API
- Postgres (read replicas)
- Normalized client store
// observability
- OpenTelemetry
- Grafana Cloud
- Sentry
- PagerDuty
// One Durable Object per fleet keeps the connected
// dispatchers in sync without a shared broker hop.
export class FleetRoom {
sockets = new Set<WebSocket>();
async fetch(req: Request) {
const pair = new WebSocketPair();
this.accept(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
}
broadcast(evt: DispatchEvent) {
for (const ws of this.sockets) ws.send(JSON.stringify(evt));
}
}
// 06 — design system
24 components, dense by default.
Built in Tailwind + Storybook, optimised for keyboard-first operators who need to dispatch a load in three keystrokes — not for marketing screenshots. Every component ships with a default and a hover state, and most have a dense and comfortable variant for different parts of the console.

// 07 — timeline
14 weeks, in commits.
w01 — discovery & instrumentation
Shadowed two dispatchers for a full shift. Wired OpenTelemetry into the legacy Rails app to get a real latency baseline before we touched anything.
w03 — design system v0
Built a 24-component dense-data system in Tailwind + Storybook. Optimised for keyboard-first operators, not marketing screenshots.
w05 — websocket gateway live
Hono service on Cloudflare Workers, fan-out via Durable Objects. Rails publishes to Redis; the gateway subscribes and pushes to connected sockets.
w08 — first migrated screen
Live load board behind a per-fleet flag. Two pilot fleets ran it for a week. We held the rollback line for 6 days before opening it wider.
w12 — full cutover
Last legacy screen retired. Old portal kept as read-only history for 90 days. SOC2 evidence captured throughout.
w14 — handover + retainer start
Internal team owns the codebase. We stay on as platform retainer — one senior engineer, two days a week.
// 08 — what broke first
The first thing to break wasn’t the framework or the gateway — it was assumptions about clock drift. Driver tablets in the field reported event timestamps that disagreed with server time by up to 11 seconds, and our ordering logic trusted the client clock. Two pilot fleets saw events render out of order on day three.
Fix was small (server-stamp on receive, surface a “synced” badge in the UI), but the lesson was bigger: when you move logic to the edge, every assumption about wall time becomes a thing you have to test for.
// 09 — reflection
What we'd do differently.
We over-invested in the design system in week 3. With one designer-shaped engineer on our side and two from Northvane, we should have shipped the load board first and let the system grow out of real screens. We’d have hit the cutover two weeks earlier.
We’d also keep the legacy portal in read-only mode longer than 90 days. Three different auditors asked for historical screenshots in month four, and we’d already turned the lights off.