Zipp Drive is our own product: an on-demand platform that connects customers in Tamil Nadu with verified acting drivers and call drivers. It ships as two apps — the Customer app and Zipp Drive Pilot for drivers — on both Android and iOS, built in-house on Flutter, Node.js, MongoDB and Firebase.
Building it taught us more about real-time systems than any client project had. Here is what actually mattered.
Two apps, not one app with two modes
The first real decision was whether to ship one app that switches between customer and driver mode, or two separate apps. We went with two.
A customer opens the app for two minutes to book a ride. A Pilot leaves it open for eight hours, on a phone mounted to a windscreen, burning battery and data the whole time. Those are different products with different performance budgets, different permission requirements and different store listings. Trying to serve both from one binary means every release risks breaking the other audience.
Flutter made this cheap. The two apps share a design system, the API client, the auth layer and the trip models as internal packages, while the screens and the background behaviour stay separate.
The map is the product
Customers do not judge a driver-booking app on its booking form. They judge it on whether the little car on the map is where the driver actually is.
That sounds like a location problem. It is really three problems:
- Accuracy — the GPS fix the phone gives you is noisy, especially between tall buildings and under flyovers.
- Freshness — a position that is thirty seconds old is worse than no position, because the customer trusts it.
- Smoothness — a marker that teleports every few seconds reads as broken, even when the data is perfect.
The fix for the third one is not more data. We publish the Pilot's position on a fixed interval and animate the marker between those updates on the customer's device. Fewer writes, a smoother map, and a smaller Firebase bill.
Why a live channel beats polling
Our first instinct on any "show me the latest" feature is an endpoint and a timer. For live tracking that is the wrong shape.
Polling gives you the worst of both: latency when you poll too slowly, and wasted requests and battery when you poll too fast — and it is the customer's phone paying for it either way. A subscription is the honest model. The Pilot app writes its position; every customer watching that trip is pushed the change.
The same channel carries trip state, which turned out to be the bigger win. A booking moves through requested, accepted, arriving, started and completed. Both apps subscribe to that state rather than asking about it, so the Pilot tapping "arrived" updates the customer's screen immediately, and neither app has to guess.
Offline-first is a decision, not a feature
You cannot assume connectivity in a moving vehicle. A Pilot loses signal in a basement car park, a lift, an underpass — routinely, and always at the moment they are trying to mark a trip complete.
So the Pilot app treats the network as unreliable by default:
- Actions are written locally first, then synced. Tapping "trip started" updates the UI immediately and queues the write.
- Location fixes collected while offline are buffered and flushed when the connection returns, rather than dropped.
- Every write is idempotent, because a flaky connection means the same request will arrive twice. The client generates the operation ID; the server treats a repeat as the same event, not a second one.
That last point is the one we would emphasise to anyone starting a similar build. Retry logic is easy to add later. Making writes safe to repeat is an architectural decision you want to make on day one.
Trip state belongs on the server
It is tempting to let the two apps negotiate a trip between themselves over the real-time channel. Do not. Fares, commission, cancellation rules and trip history all have to agree, and two mobile clients cannot be the source of truth for money.
The Node.js service owns the trip lifecycle and MongoDB holds the record. Fares are calculated server-side, the earnings dashboard in the Pilot app reads from the same records the customer's receipt does, and JWT-based auth keeps the two app audiences scoped to what they are allowed to see. The real-time layer is a transport for state the server already owns — not a second database.
Trust features are engineering features
Verified drivers, in-app ratings, trip receipts, an SOS contact, the availability toggle that lets a Pilot go offline — it is tempting to treat these as product polish and schedule them late.
They are not polish. A rating system needs a trip record that cannot be edited after the fact. An SOS feature needs the current location to be available even when the app is backgrounded. An availability toggle needs to interact correctly with dispatch, so a Pilot who goes offline mid-request does not leave a customer waiting on a driver who will never arrive. Each one reaches back into the data model. Building them late means changing the data model late.
What we carried into client work
Three things, and they apply well beyond driver booking:
- Model the state machine before designing a single screen. Every bug we chased for more than an hour came from a trip state we had not named.
- Decide what happens with no signal, feature by feature. "It shows a spinner" is an answer. Not having an answer is not.
- Budget for map and location costs from the start. They scale with usage in a way that is easy to miss in a prototype and impossible to ignore at volume.
Zipp Drive is live on Google Play and the App Store, serving Tamil Nadu. If you are building something with live tracking, two-sided dispatch or offline requirements, talk to us — we have made these mistakes already.