Designing a Complete Delivery-Management System as a Solo Developer

TIL that a system spanning a browser extension, a React Native app, an API, and a database only stayed maintainable solo because I treated the API as the one real product and everything else as a thin client of it.

3 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

The full system — a Chrome extension pulling orders from the POS, a Node.js API, a database, a driver-facing React Native app, and a dispatcher web dashboard — is really four separate client surfaces around one shared backend, all maintained by one person. Treating each surface as its own project meant duplicated logic and drift almost immediately.

Context #

Each client has a genuinely different job (extract data, dispatch, drive, view), which made it tempting to build each one's business logic locally, close to where it was needed, rather than centralizing it.

What I Tried #

Let order-status transition logic live partly in the extension (marking orders as picked up), partly in the mobile app (marking them delivered), and partly in the dashboard (manual overrides) — each surface implementing its own version of "what's a valid status transition."

What Went Wrong #

The three implementations drifted: the mobile app allowed a transition the API didn't validate, which meant a delivery could reach a state the dashboard's own logic didn't expect and rendered incorrectly.

The Solution #

Moved all status-transition rules into the API as the single source of truth, and made every client — extension, mobile app, dashboard — a thin layer that calls the API and renders its response, rather than independently deciding what's valid.

// API owns the rule; every client just calls this and trusts the result
function canTransition(from: OrderStatus, to: OrderStatus): boolean {
  const validTransitions: Record<OrderStatus, OrderStatus[]> = {
    pending: ["assigned"],
    assigned: ["out_for_delivery"],
    out_for_delivery: ["completed", "failed"],
    completed: [],
    failed: ["assigned"],
  };
  return validTransitions[from]?.includes(to) ?? false;
}

Why It Works #

With one person building four clients, the only way business logic doesn't drift is if there's exactly one place it's allowed to live. Clients that only render API responses can't disagree with each other about what's valid, because none of them are deciding that independently anymore.

Lessons Learned #

"Solo developer" doesn't mean fewer architectural constraints — it means the constraints have to be structural, because there's no code review from a second person to catch three slightly different versions of the same business rule.

What I Would Do Differently #

I'd design the API as the sole owner of business rules before writing the first client, instead of letting each client accumulate its own logic and later having to hunt down where they'd diverged.

Single source of truth for business logic, thin-client architecture, state machine modeling for order status.