Ubii is a ride-hailing platform built for the South African market: a rider app for booking trips and a separate driver app for accepting them, earning, and managing payouts. It's live on the App Store today, but it didn't start clean, and we'd rather tell you what actually happened than write the polished version.
What the brief actually was
The founders came to us with a clear gap they'd identified rather than a vague "build us an Uber competitor" brief: drivers in certain areas were unhappy with commission structures on the dominant platforms, and there was room for a more driver-friendly alternative. The ask was two apps, Ubii Ride for passengers and Ubii Drive for drivers, sharing one backend, with live GPS tracking, in-app payments, and a payout system drivers could actually trust.
The stack
We built both apps in Flutter to keep iOS and Android in sync from one codebase, with a Node.js backend and PostgreSQL as the database of record. Driver location updates and trip state propagate to the rider's screen over WebSockets, close to real time. Payments run through Paystack, and Google's Maps and Directions APIs handle routing, ETAs, and fare estimation.
The part that didn't work the first time: matching
The hardest problem wasn't the UI. It was the dispatch logic that finds an available driver near a rider and offers them the trip. Our first version used a simple radius query: find all online drivers within X kilometres, sorted by distance, offer to the closest one. In testing with a handful of drivers this looked fine. It fell apart once we simulated more realistic conditions:
- Drivers near the edge of a busy area would get flooded with offers while drivers a few hundred metres further out got nothing, because "closest" doesn't account for actual road distance or traffic.
- A driver could accept a trip, then have their connection drop for a few seconds during the handshake, leaving the rider staring at a spinner.
- Two riders requesting trips at almost the same moment could both get offered the same driver before the first acceptance was confirmed.
We rebuilt matching around a short offer-and-timeout cycle instead. A driver gets a fixed window to accept, the offer expires and moves to the next nearest driver if they don't, and a trip is only locked in once the backend confirms a single acceptance. It's a more boring solution than anything clever, and it's the one that actually held up.
Background location and battery drain
Driver apps need to keep reporting GPS location even when the app isn't in the foreground, since riders need to see the driver moving on the map while they're waiting. Naively polling location every couple of seconds in the background is an easy way to drain a driver's battery in a few hours, which is a fast way to lose drivers who need their phone all day. We tuned location update frequency based on trip state: frequent updates during an active trip, much less frequent when a driver is idle but online. That cut battery impact significantly without making tracking feel laggy to riders.
Payments and payouts
Riders pay in-app through Paystack; the platform takes a commission and the remainder needs to reach drivers reliably on a predictable schedule. This sounds simple until you have to handle a cancelled trip mid-payment, a card payment that fails after a driver has already started the trip, or a refund that needs to be reflected correctly across both the rider's history and the driver's earnings statement. We built the payout ledger as its own internal system rather than bolting it onto the trips table. Every transaction (fare, commission, refund, adjustment) is its own record in Postgres, which made reconciliation and driver-facing earnings statements far easier to get right and to debug when something looked off.
The two-sided marketplace problem
You can't properly test a ride-hailing app with no drivers online and no riders requesting trips. It's a cold-start problem that no amount of QA in isolation solves. We worked around it by building an internal simulation mode that could spin up fake driver locations moving along real roads, so we could load-test matching and tracking before a single real driver had the app installed. It's not a substitute for real-world usage, but it caught several matching and tracking bugs that would have been ugly to discover with actual drivers mid-shift.
Where Ubii is now
Both apps are live, Ubii Ride for passengers and Ubii Drive for drivers, running the matching, tracking, and payout systems described above in production. It's still actively evolving; ride-hailing is one of those products where the second year of refinement matters as much as the first build.
Building something with real-time or marketplace complexity?
If your app needs live tracking, matching, or a two-sided marketplace, we've already solved most of the hard parts once.
Talk Through Your Build