When RideOn™ came to us, the brief was deceptively simple: build a taxi app for Makkah and Madinah where passengers always know their fare before they book. No surge pricing. No surprises.
Simple to say. Genuinely hard to build — especially when your users include millions of pilgrims from 180+ countries, many of whom are navigating a foreign city on congested networks while speaking a language other than Arabic or English.
Here's how we built it.
The taxi experience near holy sites during Hajj and Umrah seasons is chaotic. Demand spikes unpredictably. Networks degrade. International visitors are unfamiliar with local pricing norms. Dynamic pricing — the standard in most ride-hailing apps — turns into a trust problem: passengers feel exploited, drivers game the system, and everyone loses confidence in the platform.
The client brief was specific: location-based fixed fares, calculated from predefined zones. Before a passenger confirms a booking, they see the exact fare. After confirmation, that fare is locked. No recalculation. No surprises.
The core promise: passengers always know exactly what they'll pay before they book.
We chose our stack to solve the specific constraints of this environment:
| Layer | Technology | |-------|-----------| | API/Backend | NestJS | | Data Store | MongoDB | | Mobile (iOS & Android) | React Native | | Cache / Sessions | Redis | | Real-Time Events | MQTT |
NestJS gave us the discipline and modular architecture needed for a production system — clean separation between auth, fare engine, booking, location, and notification modules. It scales horizontally and enforces structure in a way that Express alone doesn't.
MongoDB was the right choice for geospatial data. Its native 2dsphere indexing lets us resolve a GPS coordinate to a fare zone with a single query. Flexible schemas also meant we could iterate on zone definitions and fare matrices without migrations.
React Native let us ship iOS and Android from a single codebase — and target four distinct app builds (Rider iOS, Rider Android, Driver iOS, Driver Android) without four separate teams. RTL/LTR support is first-class in React Native, which matters when you're supporting Arabic.
MQTT is purpose-built for unreliable networks. Unlike WebSockets (which drop silently), MQTT has QoS guarantees, persistent sessions, and automatic reconnection. When a driver's phone loses signal near a crowded mosque, the session survives the interruption.
Redis handles sub-millisecond fare lookups. Fare data is pre-computed and cached — a zone-pair lookup that would otherwise require multiple database reads happens in a single Redis GET.
The fare engine is the heart of the system. Here's how it works:
Zone resolution — The passenger's pickup coordinate is matched against GeoJSON polygons stored in MongoDB using a $geoIntersects query. This resolves the pickup to a named zone (e.g. "Masjid Al-Haram Precinct").
Destination lookup — The same process runs for the drop-off coordinate.
Matrix lookup — A pre-computed fare matrix (origin zone × destination zone = fare in SAR) is stored in Redis. The lookup is a single Redis GET, returning the fare in under 20ms.
Fare lock — Once the passenger confirms, the fare is written immutably to the booking record. No driver or system action can change it after confirmation.
// Resolve coordinate to zone
const pickupZone = await this.zoneService.resolveZone(pickup);
const dropZone = await this.zoneService.resolveZone(dropoff);
// Cached fare lookup
const fareKey = `fare:${pickupZone}:${dropZone}`;
const fare = await this.redis.get(fareKey);
The result: passengers see their exact fare in the UI within 20ms of entering their destination. No spinners. No "calculating…".
The problem: WebSocket connections drop silently when phones switch cell towers in crowded areas. A dropped connection means missed location updates and lost booking state.
The solution: MQTT with QoS Level 1 (at-least-once delivery). The broker persists messages until the client acknowledges receipt. When a driver's phone reconnects, it receives all missed events automatically. No data loss.
The problem: During Hajj, tens of thousands of users authenticate within a short window. Hitting MongoDB for every OTP check and session validation would be catastrophic.
The solution: All auth tokens and OTPs are stored in Redis with short TTLs (5 min for OTPs, 24h for sessions). MongoDB is only touched for user profile data — not for every request in a session.
The problem: Rider and Driver apps have fundamentally different UX flows. Maintaining four native codebases (Rider iOS, Rider Android, Driver iOS, Driver Android) was not viable for a single team.
The solution: Build-time flags and React Native context providers. The app detects its build target at startup and renders the appropriate flow. Shared components (maps, booking cards, chat) are in a common library consumed by both experiences.
RTL/LTR is not an afterthought. The app needed to feel native in Arabic — right-to-left layouts, Arabic numerals where appropriate, and culturally correct date/time formatting. We built RTL support from day one rather than retrofitting it.
Fixed fares as a trust mechanism. Passengers in unfamiliar cities don't just want cheap rides — they want predictable rides. Our user research confirmed that passengers were willing to pay slightly more than a dynamic-pricing equivalent if they could see the fare upfront. Certainty has real value.
MQTT over WebSockets for mobile. This was the right call, and we'd make it again. The reliability improvement in degraded network conditions was significant enough that we consider MQTT the default for any real-time mobile use case.
| Metric | Value | |--------|-------| | App targets from one codebase | 4 | | Fare calculation latency | <20ms | | Cities live at launch | 2 (Makkah & Madinah) | | Surge pricing events | 0 |
RideOn™ is live in Makkah and Madinah, with expansion to additional Saudi cities in planning. Upcoming features include scheduled rides, multi-passenger booking, and a driver earnings dashboard.
On the infrastructure side, the team is moving to a clustered MQTT setup ahead of the Hajj season peak — when user volume can increase 10× within days.
Building a mobility or logistics platform? We've done this before. Let's talk →
Want to build something like this?
Start a Project →