fleet-operations console · dispatcher source-of-truth

Dispatch

Next.js + Postgres + Mapbox · 6 role portals · 9 access tiers · 255-input model

Built for:
Operators running mobile-fuel fleets where the dispatcher is the source of truth and every other role’s screen is a view into that truth.
Not built for:
Drivers who want a separate native app. The dispatcher console is the product; everything else is a portal into it.

You had four trucks on a whiteboard. Now you have forty, you’re running two shifts, and the whiteboard is fiction by 9 AM. Dispatch replaces it with a single console where the dispatcher sees what the driver, the customer, the operations lead, and the accountant each see — from one place, in real time, with one history.

§ I

The problem

A mobile-fuel operation has six roles that each need a different view of the same fleet — dispatcher, driver, customer, operations lead, accountant, owner — and historically each role gets a separate tool, a separate login, a separate audit trail. The seams between those tools are where the work falls through. A driver completes a delivery; the dispatcher doesn’t know for ten minutes; the customer’s ETA is wrong; the invoice is late.

Dispatch is the opposite shape. One backend, one source of truth, six tailored portals on top of it. Every role sees the same data through their own role’s lens, with access scoped by tier. When a driver completes a delivery, every other portal updates in the same tick.

§ II

Decisions

Three calls that shaped the scope.

  1. cut

    A separate native driver app. The dispatcher console is the source of truth — if every operator needs a second app, the console failed to be that source. Drivers get a portal-flavoured PWA; it covers what they actually need on the road.

  2. kept

    Nine access tiers, not three. Three was elegant; nine matches how authority actually distributes in a real operation, where the difference between “view a route” and “reassign a route” matters and the difference between “reassign” and “reprice” matters more.

  3. kept

    A 255-input financial sandbox living inside the operations portal — not a separate spreadsheet. The owner’s questions about pricing, margin, fleet expansion, fuel-cost shocks all resolve against live data, in the same tool that runs the day’s deliveries.

§ III

System

COREPostgressingle source of truth · WAL · prismaL59-tier access gaterole × tier · scoped capabilityDriverCustomerOperationsAccountantOwnerDispatcherUIMapbox GLlive truck tracking · ETAsCALCSandbox255 inputs · scenario diffLOGAudit historyevery role × every action × timearrows = read · writes go through L5 only
FIGURE 1. One Postgres, six portals, nine tiers — the dispatcher console (brass) is the source of truth; every other role’s screen is a typed view into the same data.
Stack — current pins.
LayerImplementationPurpose
FrontendNext.js 15 + React 19Six role-shaped portals on one app shell
APItRPC + ZodTyped contracts shared between roles
AuthNextAuth + 9-tier scopesPer-role + per-tier capability gates
DatabasePostgres + PrismaFleet · routes · deliveries · invoicing
MapsMapbox GLLive truck tracking · geofence ETAs
SandboxCustom calc engine255 inputs, cached, scenarios diffable
dispatch/server/portals/driver.tstypescript · driver portal query
// Every portal is a typed view over the same Postgres core.
// The dispatcher sees all of it; every other role sees a tier-
// scoped slice. Adding a portal is one Prisma query + one Zod
// shape — the rest of the system doesn't change.
export const driverDay = procedure
  .use(tier({ min: 'driver:read' }))
  .input(z.object({ shift: z.string() }))
  .query(async ({ ctx, input }) => {
    const stops = await prisma.delivery.findMany({
      where: {
        assignedTo: ctx.user.id,           // viewAs scopes here
        shift: input.shift,
        status: { in: ['ready', 'enroute', 'onsite'] },
      },
      include: { customer: true, route: true },
      orderBy: { sequence: 'asc' },
    });
    return stops.map(toDriverShape);       // strip ops/owner fields
  });
response · driver app payloadjson
{
  "shift": "<yyyy-mm-dd>-AM",
  "stops": [
    {
      "id": "del_4f2a",
      "seq": 3,
      "customer": "Riverside Logistics",
      "address": "1842 NE Marine Dr, Portland",
      "eta": "08:42-07:00",
      "gallons_planned": 220,
      "status": "enroute"
    }
  ],
  "_role": "driver",
  "_tier": "driver:read",
  "_redacted": ["pricing", "margin", "owner_notes"]
}
FIGURE. Same Postgres core, role-shaped response. The dispatcher sees pricing and margin; the driver doesn't. Tier check happens before the query, not after.
Dispatch fleet operations console at 7:42 AM PT — eight active trucks, route, speed, fuel, status. Pickups list and Pacific Northwest map below.
FIGURE 1. The dispatcher console mid-morning. Eight trucks live, two on low-fuel watch, two idle. Every other portal — driver, customer, operations, accountant, owner — is a tier-scoped view of this same table.
Dispatch driver mobile companion app — current stop card with bold mono delivery details, remaining stops listed below, and a four-button toolbar of START / ARRIVED / DEPART / NOTES at the bottom.
FIGURE 2. The driver’s view. Same delivery, same Postgres row, no pricing column — and the four buttons that move the row forward in the day. The dispatcher console is the source; the driver app is a cursor through it.
Dispatch role-portal split-view — the same delivery shown three different ways across operations lead, dispatcher, and accountant columns, with a single access-denied warning row in the accountant column.
FIGURE 3. One delivery, three roles. The same Postgres row read three different ways. The accountant’s column shows the one row tier denied — a billing line they aren’t cleared to see, surfaced as a row instead of an empty cell.
Dispatch audit log — thirty rows of timestamped actor/action/target events with a small per-event-type sparkline on the right rail, and a filter showing actor:b.ortiz AND ts:>06:00.
FIGURE 4. Every state change is a row. Tier promotions, deliveries closed, access denials, manual stop reorders — auditable without reconstructing intent. The right rail buckets the last hour into the colors of the action verbs.

Acknowledgments

Dispatch stands on Next.js, Prisma, Mapbox, Postgres, and the operations team that lent their whiteboards as the reference design for what the console had to replace.

← Index