Things I build outside of work — nothing related to my day job.
I have been riding bikes for 20 years — starting with cross-country mountain biking on the trails around Edmonton, Canada, then moving to Woodhill Forest in Auckland, and eventually settling into road cycling. Mostly commuting, but with the occasional longer adventure: I have ridden Seoul to Busan (500+ km) twice.
Winter and rainy seasons are the catch. I tried a few indoor cycling platforms but none of them let me ride the routes I actually wanted. So I built my own.
The system is split into five areas: infrastructure (Railway + PostgreSQL), auth and billing (Supabase, Resend, Stripe), maps and media providers (Mapbox, Cesium, Google Maps, YouTube), browser-side hardware integration, and app product subsystems.
A deliberate constraint: the entire hosted stack runs for under $5 a month. That shaped every infrastructure choice — Railway for hosting and PostgreSQL, Supabase for auth (free tier), and Stripe in sandbox mode until there's a reason to go live.
How it fits together
Why the browser owns the Bluetooth connection
The app runs in the cloud, but the trainer and heart rate monitor are physically next to the rider. A cloud server cannot reach those devices directly — so the browser owns the Bluetooth connection and sends control commands to the trainer locally, while staying in sync with the hosted app. This is what makes remote hosting actually work for a real rider setup.
I was first introduced to sailing many years ago by a friend, and something about it immediately stuck with me. Years later, a conversation with a colleague nudged me to finally give it a proper try — I completed Level 1, took a break, then came back for Level 2. That's when things started to get challenging.
Understanding wind direction, sail position, and the tiller all at the same time was surprisingly confusing. My colleague and I even built a small toy yacht to try to visualise how it all worked.
The toy yacht we built trying to get our heads around it.
It helped a little — but not enough. So I built an app instead. What started as a personal learning tool turned into something I hope helps other beginners too.
The app is currently in closed testing — if you'd like to participate as a tester and share your feedback, I'd love to hear from you. Get in touch.
Download Expo Go, then scan to try it on your phone.
Riding with friends is more fun when you can actually talk to each other — but on the road, anything beyond shouting distance is a lost conversation. Motorcyclists solve this with dedicated helmet intercoms like Cardo or Sena. I wanted the same thing for group rides, without buying radio hardware for everyone.
So I built Mesh: an open-line voice intercom for cyclists riding together. Two or more phones join the same voice channel for the duration of a ride, and everyone just talks — no push-to-talk, no taking your hands off the bars. It's intentionally small: built for me and a few friends, with no sign-up, no accounts, and no growth plans.
It's had its first real road test: a ride with my brother, both of us on bone-conduction earphones. That combination turned out to be the sweet spot — an open line to each other the whole ride, while our ears stayed uncovered to hear traffic and everything else around us.
About the name: the initial vision was a true mesh network — riders' phones relaying for each other, the way Cardo's radio intercoms do. The implementation landed somewhere more modest: a star topology through an SFU. By then the name had stuck, and I didn't bother changing it. The off-grid mesh remains the long-term dream.
Two data planes. A tiny token service on Railway is the control plane: when a phone joins a channel, it mints a short-lived access token for the voice room. The media plane is WebRTC — each phone streams its audio once to LiveKit's SFU, which forwards everyone's audio to everyone else in the channel. The backend never touches the audio.
Why there's no server-side state
The backend's only job is to hold the LiveKit API secret and issue room tokens — no database, no user accounts. Each phone stores the rider's name and channel list locally. For a tool built for a handful of friends, every piece of server-side state you don't have is a piece you never have to secure, migrate, or pay for.
One honest limitation: it's not an off-grid solution — every phone needs cellular or Wi-Fi coverage. In urban riding that's nearly always true; on remote trails, buy the Cardo.