Zoomaa
Intercity bus booking, live tracking, and a full operator back-office
Feb 2026 — Present · Accra, Ghana
Watch it work
In one paragraph
Zoomaa is a two-sided platform for Ghana's intercity bus market. Passengers search routes across operators, pick an exact seat, pay by mobile money and board with a QR ticket. Operators run their entire company from a phone — stations, routes, fleet, seat layouts, schedules, dated trips, online and counter sales, boarding, settlements and analytics — over a single multi-tenant Go and MongoDB backend. Both apps are live on the App Store.
The problem
A Ghanaian intercity bus operator runs a cash business against a paper notebook. Concretely, that means four things are broken at once:
Their market is whoever physically walks up. A bus company's reach is capped at the people who reach the loading bay. Nobody in another city, and certainly nobody abroad booking for family, can buy a seat.
Money is invisible until midnight. Cash is counted against memory at the end of the day. Leakage is undetectable because there is no record to compare against.
Nothing runs right unless the owner is standing in the yard. Delegation means handing over control, so owners don't delegate.
The business is a hustle, not an asset. It earns only while the owner is present, and it cannot be valued, borrowed against or handed down because there is no verifiable record of what it does.
Those four map exactly onto the four commitments the product is built on: the operator stays in command, money must be seen and certain, no operator is trapped in their yard, and a bus company should be wealth rather than a hustle. Every feature below traces to one of them.
What I built
Zoomaa — the passenger app. Search a corridor across operators, compare departures, pick a specific seat on a real seat map, pay by mobile money or card, and board with a QR ticket.
Zoomaa Partner — the operator console. Register the company and its stations, build routes, add buses and draw each one's seat layout, publish schedules that generate dated trips, sell online and at the counter, board passengers, take settlements, read analytics.
One Go (Fiber) + MongoDB backend serving both apps, multi-tenant, with operator-scoped JWT and role-based access isolating every tenant's data and actions.
The operator app follows the order a company is actually run in, not the order the data model was designed in: network → fleet → seat layouts → schedules → trips → sales → boarding → settlements → analytics. That sequencing is deliberate. An owner being shown the app for the first time should recognise their own week in it.
The hard part: selling a seat exactly once
This sounds mundane and is the single most important correctness property in the product. Two passengers must never buy the same seat — including when they tap at the same instant, on a flaky mobile network, with the client retrying because it didn't see a response.
The fear every operator holds about online booking is precisely this: that it will collide with the counter. So the guarantee has to be structural, not best-effort.
Document-level guarded writes with unique indexes, so the database itself refuses a double allocation rather than the application trying to be careful.
Expiring holds. A seat is held while payment is in flight and released automatically if it never completes, so an abandoned checkout doesn't strand inventory.
Idempotency keys on the booking path, so a retry after a timeout returns the original result instead of buying a second seat.
A graceful 409 when a race is genuinely lost. The counter agent is told clearly that the seat went a fraction of a second ago and picks again — which is a good day, not an error.
The seat map carries real state, not a binary: available, selected, held-by-you, booked-female and booked-male are distinct, because gender-aware seating is a real expectation on these routes and pretending otherwise makes the map wrong.
Money that can be seen
Every cedi should be visible, counted, and in the operator's account — no guesswork, no leakage, no counting cash against a notebook at midnight.
Payment is a split flow across mobile money and card through a licensed gateway, and Zoomaa never custodies funds — the operator's share settles to the operator. That is a deliberate regulatory and trust decision as much as a technical one.
Counter sales matter as much as online ones. An agent taps seats on the same live map, enters name and phone, and confirms; the booking is immediate, marked as cash, and produces a reference. The seat disappears everywhere, instantly, including from the passenger app. Cash still arrives as cash — but it is now accounted cash: which agent, which seat, which passenger, what time, how much. Nightly reconciliation stops being a memory exercise.
I'm precise with operators about what this does and does not do. It does not stop an agent pocketing cash — no software does. It makes the pocketing visible, because every seat that left the bus has a record attached to a person.
Built for the network that actually exists
Boarding is where connectivity assumptions turn into a queue of angry passengers at five in the morning. Lorry parks have poor signal, and a system that stops working when the network drops is worse than no system at all — this is the exact point at which every previous attempt to digitise boarding in this market has died.
So boarding is offline-first. When a scan cannot reach the server, the app queues it locally and keeps going. The queue survives the app being closed and the phone restarting. When signal returns, one tap syncs and every queued scan is re-validated properly server-side — the offline path is a deferral, not a bypass.
A scan resolves to one glanceable colour, because the person scanning has a queue in front of them:
Result | Meaning |
VALID (green) | Genuine, paid, for this trip, not yet used — let them on |
ALREADY USED (amber) | This ticket was scanned before; two people are trying to travel on one |
INVALID (red) | Not a valid ticket for this bus |
A reference can be typed by hand when a screen is cracked or a code won't scan. Fare evasion by duplicate ticket simply stops working, and the boarded count is live rather than counted twice and disputed.
Routing and timing
Routes are modelled on OSRM with along-corridor stop suggestions, so an operator building a route gets sensible intermediate stops rather than a blank form. Trip timing is denormalised per stop — every stop carries its own computed arrival — which makes the boarding and passenger-facing reads fast and simple.
That denormalisation includes DST-correct cross-border timing, which is not a hypothetical on these corridors: a bus crossing a border during a transition must not silently shift every downstream stop by an hour.
Delegation without handing over the keys
An owner who cannot delegate is chained to the yard, so roles are a first-class feature with a fixed capability set each:
Role | Can do |
Admin | Everything — network, fleet, schedules, pricing, staff, settlements, analytics |
Station agent | Sell, board, and run the day-to-day at their own station |
Driver | See assigned trips and the passenger list, share live location |
Authorisation is enforced server-side on every request, carrying both the operator and the person's role. The client-side gating is a convenience so people aren't shown buttons they can't use — it is not the security boundary. Broken object-level and function-level authorisation are the top two entries in the OWASP API Security Top 10, and the only defence that survives a hand-crafted request is the server checking every time.
Live operations
Drivers share location on an active trip; the owner sees where every bus is right now, and the passenger watches their bus approach rather than guessing — which also means they walk to the bus at the station rather than searching for it. Positions render on Mapbox over a WebSocket connection.
The operator dashboard carries an Action Centre that surfaces only what needs a decision now — a departure inside its final window, a trip still carrying unsold seats, a delay — and counts down the time left to act on each. Push notifications reach the owner on the phone even when the app is closed. The live sales feed shows bookings newest-first as they happen, tagged by source, with an audible alert when a seat sells.
Results
Two apps live on the App Store — Zoomaa for passengers, Zoomaa Partner for operators — plus a shared backend.
Atomic seat booking with idempotent retries and graceful 409s: no double sale, under concurrent online and counter load.
Offline-first boarding that survives total signal loss and reconciles automatically.
CI/CD auto-deploying to Azure Container Apps on every merge, with operational documentation maintained across three apps and one backend.
What I would do differently
I modelled trips as fully denormalised documents early. Reads are fast and the boarding path is simple, which was the right call for the hot path. The cost is that a schedule edit has to fan out across every generated trip, and that fan-out gets more delicate as the number of published trips grows.
Starting again I would separate the schedule template from the generated trip much more sharply on day one — template as source of truth, trip as a materialised projection with an explicit regeneration path — rather than retrofitting that boundary once the fan-out started to hurt.
Stack
Go · Fiber · MongoDB · Flutter · Riverpod · WebSocket · Mapbox · OSRM · Firebase · Azure Container Apps · mobile money and card via a licensed gateway