Case study · In progress
Musafir
In progressWhat is worth stopping for, on the road you are already taking.
A React Native travel app. Destination apps tell you where to go; Musafir searches the road itself. Search Delhi to Triund and it returns the viewpoints, temples, treks and food stops along the way, ranked by how far off the road each one really is.
- Places seeded
- 1,803
- Photos stored
- 3,141
- Places with a real photo
- 47.4%
- Closest stops get real detour times
- 20
- React Native
- Expo
- TypeScript
- Bun
- PostGIS
- Prisma
- S3
- MapLibre
The problem
The job the product exists for: help me discover what is worth stopping for along a route I am already taking, not just at the destination. Maps and review apps are organised around places you already know you want. The good stuff on a road trip is usually something you drove past.
The bet is that routes overlap. A search from Nagpur to Delhi shares road with the popular Delhi to Chandigarh corridor, so what one traveller finds on that stretch should be useful to the next one who drives it.
Who it is for
These are personas from the product brief, not measured users yet: the weekend traveller planning a short impulsive trip, the road-trip photographer after scenic stops, the family memory keeper, and the budget backpacker after local experiences.
Constraints
- Planned on wifi before leaving, then used mid-trip with patchy connectivity, GPS on and one hand free.
- Free-tier infrastructure and free, attributable data sources.
- A battery budget of under 10% an hour while tracking GPS. That is a target, not a measurement yet.
- The product brief's own biggest risk: an empty database on day one, because nobody sees the smart part until enough routes exist.
Architecture
Mobile app (Expo)
- Route search + map
- Trip / trek tracking
- Memory upload
- Offline trek packs
Bun API server
- Corridor search
- Memories + thumbnails
- Trips & treks API
- Admin review
Data & storage
- Postgres + PostGIS
- search_route()
- S3 bucket
- Supabase Auth
External sources
- OSRM routing
- OSM Overpass
- Wikimedia
- Geoapify / Nominatim
How a route search works
- The app sends a route. If it has no geometry, the server fetches one from OSRM.
- The route is matched against road pieces saved by earlier searches, which gives an overlap percentage shown in the app.
- PostGIS finds places within a corridor around the route: 8 km, or 15 km for routes over 100 km, closest first.
- The 20 closest get real detour times from OSRM. If routing fails, a detour heuristic takes over instead of the search failing.
- Places with a detour over 30 minutes are dropped. The rest are scored on detour, quality and community signals, and ordered by where they sit along the route.
Matching roads between searches
Step 1
Split into ~500 m pieces
Equal slices of the route line
Step 2
Prefilter within 200 m
Heading within ±45°, either direction
Step 3
Sample 5 points
At least 4 within 50 m of one saved piece
Step 4
Match found
Counted towards the overlap
Step 5
No match
Saved as a new road piece
Key decisions
Route matching runs inside Postgres as a PostGIS function
- Why
- Geo-matching belongs in the database that already indexes the geometry. PostGIS already answers "what is near this line", so there is no reason to rebuild that in application code.
- Trade-off
- The logic lives in migration SQL, which is harder to test and version. No automated test covers it yet.
One loop iteration per road piece
- Why
- Simple to reason about, and fine at early traffic.
- Trade-off
- Cost grows linearly with route length: a 1,000 km route is about 2,000 iterations.
- Rejected
- A set-based query, documented in the function itself as the rewrite once long routes feel slow.
A heading check on top of distance
- Why
- Two roads crossing at one point are close for a moment but are not the same road. Requiring a similar heading, in either direction of travel, keeps a crossing from counting as overlap.
- Trade-off
- Both directions of a road match each other.
Leave a field empty rather than guess
- Why
- Every place detail comes from a free, attributable source. Anything a source does not have stays empty, and is never estimated.
- Trade-off
- Coverage is honest but incomplete: 854 of 1,803 places have a real photo.
- Rejected
- Google Places photos (a paid API) and Mapillary, both named in the pipeline reports as ways to raise coverage later.
A small Bun API between the app and the backend services
- Why
- The storage secret key must never live in the app bundle, and the mobile client only ever talks to one API.
- Trade-off
- One more service to run than calling Supabase directly, which the early design docs proposed.
- Rejected
- Supabase-direct access from the app.
Photo uploads go straight to S3, then the server makes the thumbnail
- Why
- Large files never pass through the API. The server only signs a URL, then confirms the upload and generates a 600 px thumbnail.
- Trade-off
- A two-step protocol with its own states: processing, ready or failed.
Handling a messy real world
| Situation | What the app does |
|---|---|
| Noisy GPS | Drops fixes worse than 35 m accuracy, jumps faster than 250 km/h and moves under 3 m |
| No connection mid-trip | Keeps unsent points on the device and restores an active journey on launch |
| Retried or duplicate trek points | Ignored by a unique constraint on the session and sequence |
| Routing service down | Detours fall back to a heuristic; the search still returns |
| Route matching fails | Logged as non-fatal; the search continues with no overlap |
| Upload never arrives | The server checks the object exists and marks the upload failed |
Status
| Built | In progress | Next |
|---|---|---|
| Route and corridor search with detours | Place de-duplication | Attach places to matched road pieces |
| Road-piece matching with a heading check | Route details and elevation profile | Set-based matching for long routes |
| Place catalogue and photo pipeline | Tests for route search | |
| Trips, treks and foreground GPS tracking with offline sync | Background tracking | |
| Memories, collections, notifications, profile |
Deep dive
Matching road trips that share a road, in PostGIS
How Musafir decides that two different trips drive the same stretch of road: slicing routes, sampling points, checking headings, and why the geometry and geography types both show up.
Read the post