Logistics backend, Aug 2026

LastMile IQ

A delivery platform that quotes rates, assigns drivers automatically and keeps an append-only history of every order.

Role
Solo project: schema, API, business logic, UI and deployment.
Stack
  • Next.js 16
  • TypeScript
  • PostgreSQL
  • Prisma
  • JWT
  • bcrypt
  • Tailwind CSS
  • Vercel
Links
LastMile IQ admin console: order totals, revenue and the shipment feed Public tracking page for an order, with its status timeline

The problem

Last-mile pricing depends on how big a parcel is, not only how heavy. Assigning a driver means trading distance against how busy each driver already is. And every change to an order needs a record nobody can quietly edit later.

I built the backend that handles all three, with an admin, customer and delivery-agent view on top so the rules can be seen working.

How it's built

Next.js App Router serves both the pages and the API. Route handlers check the JWT cookie and the caller's role, then hand off to plain TypeScript services that own the business rules. Prisma is the only thing that talks to PostgreSQL.

LastMile IQ request path Clients to API routes (HTTPS). API routes to Services. Services to Prisma ORM. Prisma ORM to PostgreSQL Clients Admin Customer Delivery agent API routes 16 route handlers JWT cookie session Role check per route Services Rate engine Assignment engine Order lifecycle Notification log Prisma ORM typed queries PostgreSQL 9 models HTTPS
Request path from the three client roles down to the database.

Key decisions

Bill on whichever is heavier: the scale or the box

Couriers charge for the space a parcel takes up. The rate engine computes volumetric weight as length × width × height / 5000, bills the higher of that and the actual weight, rounds extra kilograms up and never goes below the card's minimum charge.

Rates live in the database as cards keyed by customer type (B2B or B2C) and route (same zone or cross zone), so admins change prices without a deploy. Cash-on-delivery adds a surcharge that is either a fixed fee or a percentage of the declared value, with a floor.

const volumetricKg = (lengthCm * widthCm * heightCm) / 5000;
const chargeableKg = Math.max(actualWeightKg, volumetricKg);

const extraKg = Math.max(0, chargeableKg - rateCard.baseWeightKg);
const shippingCharge = Math.max(
  rateCard.minCharge,
  rateCard.baseRate + Math.ceil(extraKg) * rateCard.perExtraKgRate,
);
Condensed from src/lib/services/rate-engine.ts

Score drivers instead of picking the nearest one

The nearest driver is often the busiest. Assignment first drops anyone at capacity, then scores the rest: great-circle distance to the pickup (Haversine), plus 25 km if the driver is outside the pickup zone, plus 2 km for every delivery they already hold. The lowest score gets the order.

Admins can override the choice by hand, and the override moves the load counters between drivers so capacity stays accurate.

const eligible = agents.filter((a) => a.activeDeliveries < a.maxCapacity);

const scored = eligible.map((agent) => {
  const distanceKm = haversineKm(agent, pickup);
  const zonePenalty = agent.currentZoneId === order.pickupZoneId ? 0 : 25;
  const workloadPenalty = agent.activeDeliveries * 2;
  return { agent, score: distanceKm + zonePenalty + workloadPenalty };
});

scored.sort((a, b) => a.score - b.score); // lowest score wins
Condensed from src/lib/services/assignment-engine.ts

Make order history append-only

Status changes go through one lifecycle service. It checks the move against a table of allowed transitions, stops agents from touching orders that aren't theirs, and writes a tracking event with the actor, their role, a note and coordinates. No route updates or deletes those events.

A failed delivery lets the customer pick a new date, which creates a reschedule record and runs assignment again.

Order lifecycle CREATED to ASSIGNED. ASSIGNED to PICKED_UP. PICKED_UP to IN_TRANSIT. IN_TRANSIT to OUT_FOR_DELIVERY. OUT_FOR_DELIVERY to DELIVERED. OUT_FOR_DELIVERY to FAILED (attempt fails). FAILED to RESCHEDULED (customer picks a date). RESCHEDULED to ASSIGNED (re-assigned). CREATED to CANCELLED CREATED ASSIGNED PICKED_UP IN_TRANSIT OUT_FOR_DELIVERY DELIVERED CANCELLED RESCHEDULED FAILED attempt fails customer picks a date re-assigned
Main path and the failure loop. The full transition table also allows a few shortcuts, and admins can override any move.

Results

route handlers over a 9-model schema
16
test assertions on pricing and the order lifecycle
17
seeded demo accounts across 3 roles
6

Deployed on Vercel with a one-click role switcher, so anyone can try the admin, customer and agent views without signing up.

Worked example from the README: a 25 × 20 × 15 cm parcel weighing 1.2 kg is billed at its 1.5 kg volumetric weight.

Limits and next steps

  • Notifications are written to a log table, not sent. Wiring a provider such as Resend is the next step, and the README should say so until then.
  • Assignment runs as separate writes. Two admins assigning at the same moment could double-book a driver, and a Prisma transaction would close that gap.
  • The tests cover pricing and one lifecycle transition. Driver assignment has no tests yet.