DEV Community

Ugo
Ugo

Posted on

How I built RaceScore: From a simple MVP to a 20,000-race endurance sports platform

Hey Dev.to community! ๐Ÿ‘‹

A year ago, I posted here presenting the early MVP of RaceScoreโ€”a side project I started to solve a simple frustration: trail runners had universal indices like Betrail or ITRA to evaluate their level, but road runners and multi-sport athletes had nothing similar.

Fast forward to today, and RaceScore has evolved from a small prototype into a full-fledged endurance sports platform with over 20,000+ races, a dynamic performance scoring algorithm, and B2B tools for race directors.

Here is a look at what changed, the tech stack behind it, and what I learned along the way! ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’จ

๐Ÿš€ What RaceScore Does Today

  1. Universal Dynamic Performance Score: Instead of just looking at raw finish times or rankings, RaceScore calculates a relative score based on race difficulty, field depth, and conditions.
  2. Interactive Event Discovery: Powered by Mapbox GL JS, athletes can explore over 20,000+ running, trail, and triathlon events with elevation profiles and GPX tracks.
  3. Ecosystem Sync & Features: Strava OAuth integration for auto-syncing race activities, an HMAC-signed ICS calendar feed, and a peer-to-peer secondhand bib marketplace.
  4. B2B Organizer Suite: Turnkey race management including Stripe online registrations, waiting list management, and QR-code bib scanning on race day for volunteers.

๐Ÿ› ๏ธ The Tech Stack
Here is what powers racescore.fr under the hood:

  • Frontend: React 19 + Vite + TypeScript
  • UI & Styling: Tailwind CSS v4 + shadcn/ui (Radix UI)
  • Routing & Data Fetching: React Router v7 + TanStack React Query
  • Backend & Database: Supabase (PostgreSQL, Auth, Row Level Security)
  • Serverless API: Vercel Serverless Functions (Node/TypeScript)
  • Mapping & Data Vis: Mapbox GL JS + Recharts
  • Third-Party Services: Stripe (Payments), Strava API (OAuth), Nodemailer/Brevo (Transactional Emails), hCaptcha

๐Ÿ” Key Technical Learnings & Security Highlights
Building a platform with thousands of public race pages and personal athletic data brought some fun engineering challenges:

  • Securing Calendar Subscriptions: To prevent IDOR vulnerabilities on user ICS calendar exports, feed URLs are signed using HMAC tokens.
  • OAuth Cookie Security: Strava authentication tokens are sealed strictly inside httpOnly cookies, keeping them inaccessible to client-side JavaScript.
  • Constant-Time Timing Protection: Internal webhooks and CRON triggers use constant-time comparison (crypto.timingSafeEqual) to mitigate timing attacks.
  • Granular Database Access: Leveraging Supabase's Row Level Security (RLS) paired with strict role checks on administrative endpoints (/api/admin).

๐Ÿ‘€ Check it out!
The app is live and fully accessible:

๐Ÿ”— Website: racescore.fr

I'd love to hear your thoughts, feedback on the performance scoring concept, or any technical critiques regarding the stack.

Happy running & coding! ๐Ÿƒโ€โ™‚๏ธโšก

Top comments (4)

Collapse
 
vollos profile image
Pon

Writing up the IDOR-signed URLs and cookie hardening before anyone had to point it out is rare to see in a build-in-public post. Curious about the bib marketplace side: when a bib gets resold peer-to-peer, does the new owner's account take over race-day scanning and results for that number, or does the original entrant stay attached to whatever the volunteer scans?

Collapse
 
publiflow profile image
PubliFlow

Scaling a side project from an MVP to handling 20,000 races is a massive milestone, especially when dealing with the complex data relationships inherent in endurance sports. I have found that structuring Supabase Row Level Security policies early on saves countless headaches when user roles and data permissions inevitably get more complex as traffic grows. If you are looking to spin up a similar data-heavy SaaS without wrestling with the initial setup, we built PubliFlow to handle Next.js and Supabase boilerplate out of the box. You can check out the starter at publiflow.vip.

Collapse
 
publiflow profile image
PubliFlow

Scaling a Supabase backend to handle 20,000 races is a massive milestone, especially when dealing with the real-time data updates that endurance sports platforms require. I found that implementing row-level security and optimizing edge functions early on saved us a lot of headaches when query volumes started to spike. It is always inspiring to see a side project grow into such a robust platform. We actually ran into similar scaling considerations when building out our own Next.js and Supabase SaaS boilerplate, PubliFlow, which really highlighted the importance of getting the database architecture right from day one. Did you end up using Supabase Realtime for the live race tracking, or did you have to implement a custom WebSocket solution for the heavier data loads?

Collapse
 
publiflow profile image
PubliFlow

Scaling a Supabase backend to handle that volume of race data usually means you have to rethink your row-level security policies and indexing strategy pretty early on. When you hit the 10,000 race mark, did you find yourself needing to implement database functions for complex aggregations, or were you able to keep most of that logic in your TypeScript backend? I have seen a lot of React apps start bottlenecking on the client side when fetching large relational datasets, so utilizing Supabase real-time subscriptions for live race updates is usually a game changer for overall performance.