SoulRide — An Offline-First Cycling Companion
What is this?
SoulRide is an offline-first cycling app I’m building with React Native and MapLibre. The whole idea is a companion for a bike ride — a native map, live radar from a BLE device, route cues for the turns ahead. No account, no upload, no cloud company sitting in the middle quietly building a profile of everywhere you’ve ridden. I really just needed two things:
- Radar integration - see and hear alerts when cars are behind you
- Route mapping and elevation profile - a head unit but without having to record anything
What it does so far:
- A native
Maptab on iOS and Android (MapLibre) with follow modes that keep the camera centered and heading-aware as you ride. - A radar overlay driven by live BLE threat data — discovery, connection, reconnection, battery reporting, and a “forget device” flow for supported radar units.
- Local GPX route import with on-device cue detection: a tile-backed mini-map that warns you about upcoming turns, kinks, overlaps, and self-crossings, all computed from the saved geometry.
- SQLite for everything personal — unit preference, saved radar metadata — and a shared app context that coordinates persistence, permissions, location, and BLE state.
Future Enhancements:
- More BLE sensors integrated (Safety and Informational)
- Louder alerts while backgrounded
To support fully offline maps SoulRide has a sibling project: the offline tiles pipeline.
The other half: the tile pipeline
The pipeline is the backend that makes the offline map possible. It uses Planetiler to build per-region .pmtiles files (OpenMapTiles schema) for all of North America — every US state, Canadian province, and Mexican state, plus Central America and the Caribbean as whole countries. That’s ~126 region files.
A few pieces that make it work:
- A low-zoom world base map (z0–6) the app draws underneath everything, so areas you haven’t downloaded are never just blank.
pmtiles extractclips each region to its actual admin boundary, so a download is exactly the size of the place you want and nothing more.- A manifest that acts as the contract between the pipeline and the app: version, schema, per-region bounds, URLs, byte sizes, and a glyphs block for the label fonts.
- Everything is published to Cloudflare R2 with immutable, version-scoped URLs and byte-range support — so a download is resumable, and bumping a version never breaks a download that’s already in flight.
In the app, you go to Settings → Offline maps, pick the regions you ride in, and they download once. After that the map renders entirely on-device. No tiles fetched mid-ride, no tracking, no dependency on anyone’s API staying up.
Why I wanted to do it
I ride a lot, and almost every cycling app wants to be the system of record for your life — upload every ride, sync it to their cloud, surface it to whoever they’ve partnered with. For most people this is fine, but I began to be disenfranchised with the idea that my data are saved all over the place, on many machines that I have no control over. I wanted a tool that’s genuinely mine. The radar data, the route, the map — it all lives on the phone and it’s mine to share or delete as I please. In my opinion, if you are just using your phone as a head unit to alert you of turns and cars, there is no need to go off-device.
It’s also a great forcing function for “offline-first” as an actual engineering constraint. When the network truly isn’t allowed to be part of the happy path, you have to think hard about what gets pre-built, what gets cached, what the failure modes are, and where the data really lives. The map problem alone — how do you ship a usable map of a continent to a phone without a tile server — turned into a whole project on its own.
What I’m learning
- Building a real BLE integration with
react-native-ble-plx: discovery, connection lifecycle, reconnection, characteristic subscriptions, and a generic handler with a device-specific adapter on top. - Native maps in React Native with
@maplibre/maplibre-react-native, including follow/heading camera modes and rendering PMTiles fully offline. - The vector-tile toolchain end to end — Planetiler, the OpenMapTiles schema,
pmtiles extract, SDF glyph generation, and hosting it all as immutable static files on R2. - Designing a manifest as a versioned contract between an app and its data so refreshes are safe and downloads stay resumable.
- On-device geometry: detecting turns, overlaps, and self-crossings from raw GPX so the route cues are computed locally instead of asking a server.
- Battery management. A phone as a map is great until you run out of battery.