← The ladderRung 06 · reference

Reference architecture

Order payment dispatch

Reference architecture for an app that takes an order, charges a card and dispatches a driver. The components, the state machines, the invariants that stop it double-charging or double-dispatching, the order to build them in, and what it costs to build and to run.

Applies to any app where software acts on a customer's behalf after payment: food delivery, courier, on-demand services, marketplace fulfilment. The pattern is the same. What changes is who the driver is.

Difficultyrung 6 of 7$750a year in platform costs, before people.
Time to trust it
a year before you trust it
Blast radius
a real person's evening, and a merchant's revenue
Reversible
No. The card is charged and the driver is moving.
Platform floor
~US$750/yr in platform costs. People and per-transaction fees are the real bill.

01

Components

  • Customer app, iOS

    Native. Swift + SwiftUI.

    Browse, cart, checkout, order tracking. Holds no business logic that matters, it can be wrong or out of date at any time.

  • Customer app, Android

    Native. Kotlin + Jetpack Compose.

    Same surface as iOS. Budget it as a second build, not a port: separate toolchain, separate store review, separate release cadence.

  • Operator dashboard

    Next.js + React, server-side auth.

    Order queue, menu and stock overrides, refunds, manual intervention. Required before launch, someone must be able to run the business.

  • Database

    Postgres, with row-level security.

    The single source of truth. All three clients read and write here and nowhere else.

  • Server-side functions

    Serverless, TypeScript. ~12 of them.

    Everything involving money, external providers, or acting on the customer's behalf. None of it is callable from a client.

  • Realtime channel

    Postgres change feed over websockets.

    Live order status to the customer app and the dashboard. Replaces polling; roughly a one-line subscription per client.

02

State machines

order_status
  • draft
  • received
  • preparing
  • ready
  • out_for_delivery
  • completed
  • cancelled
  • failed
payment_status
  • pending
  • paid
  • failed
  • expired
  • refunded
  • voided

Two independent state machines on the same order. They are not a single status field and must never be collapsed into one. Legal combinations include payment=refunded with order=out_for_delivery, the customer has been refunded and a driver is still en route. Your dashboard, your notifications and your reconciliation all need an answer for it.

  • 01Store transitions, not just current state. Append-only, with a timestamp and the actor. Support and dispute resolution are impossible without it.
  • 02Terminal states (completed, cancelled, failed) must be terminal. Guard the transition in the database, not the application.
  • 03draft exists so an abandoned cart is not an order. Do not let a client create anything past draft.

03

Server-side functions

Everything touching money, an external provider, or acting for the customer. The trust column is the part that matters.

  • create-payment

    Creates the payment intent with the provider and returns the client secret or redirect.

    Client-callable, authenticated, amount computed server-side from the cart. Never trust a price sent by a phone.

  • payment-webhook

    Receives paid / failed / expired / refunded from the payment provider.

    Public endpoint. Verify the signature, then treat it as untrusted input.

  • delivery-quote

    Asks the delivery provider what a job will cost before checkout.

    Client-callable. Cache it; quotes are rate-limited and cost money.

  • dispatcher

    Books the delivery job. The only thing permitted to create a driver task.

    Service role only. Never client-callable, under any circumstance.

  • delivery-webhook

    Receives driver assigned / picked up / delivered / cancelled.

    Public endpoint, signature-verified, idempotent.

  • delivery-status

    Read-through for the tracking UI.

    Client-callable, scoped to the caller's own orders by RLS.

  • menu-sync

    Pulls catalogue, prices and availability from the merchant's POS.

    Scheduled. Must survive the POS being down without corrupting the local catalogue.

  • order-push

    Pushes the paid order into the merchant's POS so it prints in the kitchen.

    Service role, retried, idempotent on the POS side too.

  • send-push

    Fans out status-change notifications to devices.

    Service role. Triggered by a status transition, not by the client.

  • delete-account

    Deletes the user and records that deletion was requested.

    Client-callable. Mandatory for both app stores if you have accounts.

04

Invariants

Enforce these in the database or the platform, not in application code. They are the difference between a demo and a system.

One payment per order.

Enforce
A unique constraint on payment_intents(order_id). Enforced by the database, not by application checks.
Or else
Payment providers retry webhooks. A duplicate delivery double-charges the customer or double-credits the order.

Dispatch fires once, on the payment transition, server-side only.

Enforce
Trigger dispatch off the transition into paid, not off the current status. Service-role only, never exposed to a client.
Or else
Retries or a replayed webhook send two drivers to one address, and you pay both.

Every external write is idempotent.

Enforce
Pass an idempotency key to the provider, and key your own writes on the provider's event id so replays are no-ops.
Or else
Networks time out mid-call. Without keys you cannot safely retry, and not retrying loses orders.

Every external call checks a runtime flag first.

Enforce
A config row read at request time, not an environment variable and not a build constant.
Or else
A provider outage becomes an app-store release. Review latency is ~24h; a dinner service is ~90 minutes.

Prices and totals are computed server-side.

Enforce
The client sends item ids and quantities. Nothing else.
Or else
A client-supplied total is a free-money bug.

05

Integration contracts

Payments

Outbound intent, inbound webhook.
  • , Webhooks arrive more than once, out of order, and occasionally for orders you have already terminated.
  • , Handle refunded and voided as first-class states, not error branches.
  • , Reconcile daily against the provider. Your database and their ledger will drift.

Delivery

Quote, dispatch, status webhook.
  • , Put it behind the runtime flag from day one, with a mock provider implementing the same interface.
  • , The mock lets you build and ship the whole order path before commercial terms are signed.
  • , Quote at checkout and re-resolve at dispatch. The fee can move between the two.

Merchant POS

Menu in, order out.
  • , You do not own the catalogue. Sync it, and treat every sync as potentially partial.
  • , Never delete local items on a failed sync. Mark unavailable instead.
  • , Their outage is your outage as far as the customer is concerned.

Push notifications

Outbound, per device token.
  • , Requires a signing key per platform. Keep it out of the repository.
  • , Token churn is constant. Store per-device and prune on rejection.

06

Observability

  • Product analytics and error tracking are different tools and you need both. Analytics shows a checkout abandoned; error tracking shows whether the app crashed doing it. The two look identical from the outside and need opposite responses.
  • Log every state transition with a correlation id that spans client, function and provider. Support questions are always 'what happened to order X'.
  • Alert on the transition that did not happen: paid with no dispatch inside N seconds. Absence is the failure mode on this rung, and nothing throws an exception when work simply never starts.

07

Build order

Most projects invert this and build the customer apps first. That is the most expensive decision available on this rung.

  1. 01Schema and state machines. Orders, items, both status enums, the transition log, RLS. Nothing renders until this is settled.
  2. 02Payment path in test mode, including duplicate webhooks, refunds and pay-after-close. Least fun, most expensive to retrofit.
  3. 03Dispatch behind the runtime flag, against a mock provider. Real provider becomes a config change later, not a rewrite.
  4. 04POS sync, in both directions, with partial-failure handling.
  5. 05Operator dashboard. The business must be operable before a customer touches it.
  6. 06Customer apps. Last, because they change most once the four above are real.

08

Cost to build, cost to run

List prices in USD, checked August 2026. Payment and delivery rates are deliberately absent, they are negotiated per merchant and market, and a made-up percentage is worse than none.

Cost to build

Close to zero in platform spend. Free tiers cover the entire build, and the only unavoidable line items are the developer accounts you need before you can test on real hardware. The build cost of a rung-six app is time, not infrastructure, which is why infrastructure is the wrong thing to be comparing when you scope one.

  • Apple Developer ProgramUS$99/yrRequired before TestFlight or any on-device testing. Starts the day you enrol, not the day you launch.
  • Google PlayUS$25 onceOne-off, for the life of the account.
  • Domain~US$15/yrFor the dashboard and your deep links.
  • Supabase (Postgres, auth, realtime, storage, functions)$0Free tier: 500 MB database, 5 GB egress, 50k monthly active users, 500k function calls, 2M realtime messages, 200 concurrent connections.
  • Vercel (dashboard hosting)$0Hobby tier covers a build. Read the terms before launch, it is non-commercial.
  • PostHog (analytics, replay, flags, error tracking)$0Free monthly: 1M events, 5k session replays, 1M feature-flag requests, 100k exceptions. No platform fee at any tier.
  • Push (APNs / FCM)$0Both free. The cost is the signing key handling, not the service.
  • Google Maps / PlacesusageAddress autocomplete and place details. The old US$200/mo blanket credit ended 28 Feb 2025, so this bills from the first request now, budget it rather than assuming it is free.
  • Payments and delivery sandboxes$0Test modes are free. You will live in them for weeks.

~US$140 in the first year. The rest of the build cost is time.

Cost to run

Three layers, and they behave differently. The floor is fixed and small. The usage layer steps up as activity grows. The per-transaction layer scales linearly with revenue and never stops. Most people budget the first and are surprised by the third.

Layer 1, fixed floor
  • Apple Developer ProgramUS$99/yrLapses and the app is delisted.
  • Google Play$0Already paid. No renewal.
  • Domain~US$15/yrThe only line that can kill the whole thing if you forget it.
  • Supabase ProUS$25/mo8 GB database, 250 GB egress, 100k monthly active users, 2M function calls, 5M realtime messages, 500 concurrent connections. Team tier is US$599/mo and you will know when you need it.
  • Vercel ProUS$20/moThe tier whose terms permit commercial use. Per seat, so it grows with the team not the traffic.
  • PostHog$0No platform fee. You stay at zero until you exceed the free monthly allowances.
  • Push, APNs / FCM$0Free at any volume.
Layer 2, usage, scales with activity
  • Database beyond 8 GBUS$0.125/GBOrder history and transition logs grow forever unless you archive.
  • Egress beyond 250 GBUS$0.09/GBMenu photography is the driver. Compress and cache before you scale.
  • Function calls beyond 2MUS$2 per 1MWebhooks and status reads dominate. Cheap, and rarely the problem.
  • Realtime messages beyond 5MUS$2.50 per 1MEvery status change fans out to every subscribed client.
  • Concurrent connections beyond 500US$10 per 1,000The one that bites. See below.
  • Monthly active users beyond 100kUS$0.00325 eachOnly counts authenticated users, so it lags order volume.
  • Analytics events beyond 1M/mofrom US$0.00005 eachRate drops with volume. Instrument deliberately or an order becomes 40 events.
  • Session replays beyond 5k/mofrom US$0.005 eachThe most expensive per unit. Sample it, do not record everyone.
  • Exceptions beyond 100k/mofrom US$0.00037 eachError tracking lives here too, so it is one bill rather than a separate vendor.
  • Google Maps / Placesper requestAutocomplete keystrokes are free, but the place-details call that resolves the chosen address is billed, once per checkout. Scales with orders, not with users.

≈ US$45/mo plus US$114/yr, so roughly US$650–750 a year all-in before a single order is placed. For an app that takes money and dispatches drivers, the infrastructure floor is genuinely small, which is exactly why it is the wrong number to focus on.

The capacity number that bites

Concurrent realtime connections are the capacity number to plan against, not database size. A live tracking screen holds a connection open for the whole delivery, so peak connections is roughly peak simultaneous orders in flight, plus every dashboard someone left open in the kitchen. The 500 included on a production tier is therefore a real operational ceiling: about 500 orders being watched at once. Know that number before a Friday night finds it for you, and cap it by closing the subscription when an order reaches a terminal state.

Layer 3, per transaction, scales with revenue
  • , Payment processing: a percentage plus a fixed fee on every order. Rates vary by provider, market and volume, and are negotiable once you have volume.
  • , Delivery: charged per job, whether the job succeeds or is cancelled at the door.
  • , These two are the entire cost model at scale. At meaningful order volume they dwarf every line above, and no amount of infrastructure tuning touches them.

The line nobody puts in the spreadsheet is a person who can be reached during service. Something will fail while orders are live, and the response is to flip a flag or refund a customer, not to write code. Below rung five that is an inbox you read on Monday. Here it is a roster, and it is the single largest running cost of a rung-six app.

Out of scope

  • , Running your own courier fleet. Riders, routing, and driver payouts is a separate business, and rung seven.
  • , Multi-merchant marketplace mechanics: commissions, payouts, and settlement across many sellers.
  • , Anything identifying the client build this is drawn from.

The difficulty on this rung is not code volume. It is that the system acts unattended, so every failure mode is a real-world side effect that no deploy reverses. The invariants above are the whole job.