TapRide is a full-stack ride-hailing app — riders request a trip, watch the driver approach on a live map, agree a fare up front and chat in-app. The interesting constraint I set myself was simple: no Google Maps API key, and no PostGIS. Here is how that shaped the whole architecture.
Why avoid Google Maps in the first place?
Every ride-hailing tutorial reaches for the Google Maps Platform on the first day, and then the billing alerts start. Maps, Directions and Distance Matrix all meter per request, and a ride app hammers them — every location update, every fare estimate, every map pan. For a project meant to run as a free, public demo out of Zimbabwe, a metered map bill was a non-starter.
So the rule became: build a real, production-shaped ride-hailing experience on entirely open infrastructure. That single decision turned out to be the most useful design forcing function in the project.
The map layer: OpenStreetMap + Leaflet
On the web app the map is Leaflet rendering OpenStreetMap
tiles. On Android the native app uses osmdroid, the OSM renderer for
Android. Neither needs an API key. The rider and driver markers are plain Leaflet markers
whose coordinates are driven by live data, so the "watch your driver approach" effect is
just a marker whose lat/lng updates as new positions arrive.
The web client is React 18 + TypeScript + Vite + Tailwind CSS. The Android client is genuinely native — Kotlin + Jetpack Compose in an MVVM structure — rather than a webview wrapper, so the mobile experience feels like a real app and can talk to location services directly.
Finding nearby drivers without a geospatial database
The classic way to answer "which drivers are near this rider?" is PostGIS with a
ST_DWithin query. I didn't want that dependency, so TapRide does proximity in
two cheap steps:
- Bounding box first. Given the rider's position and a search radius, compute a small latitude/longitude rectangle and query only drivers inside it. That is a plain indexed range query — fast, and supported by any database.
- Haversine second. The bounding box returns a handful of candidates, so the app then applies the haversine formula in code to get true great-circle distance and sorts by it. Running haversine over five candidates is free; running it over every driver in the country would not be.
This box-then-refine pattern is a good reminder that you rarely need the heavyweight tool. A coarse, index-friendly filter followed by an exact in-memory calculation covers the vast majority of "find things near me" features.
Real-time everything with Appwrite
The backend is Appwrite — authentication, the database, storage and a Realtime WebSocket. The data model is small and readable:
profiles— user profile, role (rider/driver) and vehicle inforides— the lifecycle document for a single tripdriver_locations— live GPS positions, updated as drivers movemessages— in-ride chatratings— post-ride ratings
The whole live experience comes from subscribing to those collections over Appwrite
Realtime. When a driver's app writes a new position to driver_locations, the
rider's subscribed client receives it over the WebSocket and moves the marker. When the
ride status changes from requested to accepted, both sides update
instantly. Chat is the same mechanism pointed at the messages collection — so
rider and driver coordinate the exact pickup without exchanging phone numbers.
Fares agreed up front
A lot of ride apps quote a range and surprise you at the end. TapRide estimates the fare from distance and route before the trip is confirmed and shows it as a single number the rider accepts. Because distance is already being computed for matching, the fare estimate reuses the same maths — no extra paid Distance Matrix call.
One product, two real clients
Keeping a React web app and a native Kotlin app in sync sounds like double the work, and it is more work — but sharing one Appwrite backend means the contract is the database schema, not a shared UI framework. Both clients read and write the same documents and subscribe to the same Realtime channels. The web app deploys to GitHub Pages on push; the Android app is built into an APK by GitHub Actions and attached to a Release.
What I'd tell someone starting this
- Pick your constraint early. "No Google Maps key" decided the map stack, the proximity algorithm and even the cost model in one move.
- Model the ride as a single document with a status field. Almost every feature — matching, tracking, chat, rating — hangs off that lifecycle.
- Let the database be the contract between clients instead of trying to share UI code across web and native.