The Idea
The data I already have, working for me instead of just sitting there.
Every run I do gets logged somewhere โ Strava, Garmin, my phone โ but that data mostly just accumulates. stride is my attempt to actually put it to work: pull in the raw workouts, spot the trends that matter (mileage ramp, pace drift, cadence changes), and recommend gear when the numbers say I need it โ not when a brand ad tells me to.
A shoe rotation prompt when I hit 400 miles. A hydration suggestion when the long runs cross 90 minutes. Race-prep nudges as a target distance approaches. Small, honest, useful.
โ Try the live prototype ยท Synthetic data, real logic
Why This One
Two reasons. First, it's mine โ building for yourself as the user is a cheat code for good product decisions. Second, it stretches the same muscles I use at work (data ingestion, analytics, recommendation logic) but in a domain I actually care about outside of a job.
Architecture
Next.js App Router, Supabase RLS, Strava OAuth โ nothing custom that wasn't needed.
The data flow: user signs in with email (Supabase Auth) โ clicks
"Connect Strava" โ OAuth handshake stores tokens in a
strava_tokens table โ server action pulls the last 90
days from Strava's /athlete/activities, normalizes each
run into an activities row, and upserts with a
(user_id, source, external_id) conflict key so
re-syncing is idempotent.
Row-Level Security is on for every user-owned table
(profiles, strava_tokens,
activities, shoes). Users can only ever
read or modify their own rows โ the guarantee is enforced by
Postgres, not by application code that could forget to filter.
The Dashboard
Server components do the heavy lifting: the page loads, pulls the
user's activities from Supabase, and computes stat cards + chart
series in pure functions before the HTML is streamed to the browser.
Recharts renders the weekly-mileage bar chart and pace-trend line
client-side; a small km โ mi toggle is the only
stateful bit. Everything else โ the totals, the recent-runs list, the
shoe-mileage bars โ is fully server-rendered.
Splitting the aggregation into a
lib/stats.ts module of pure functions
(computeStats, weeklyMileage,
paceTrend) meant I could reason about the math
independent of the rendering. Every function is a plain
(activities, options) โ derived โ trivial to unit-test,
trivial to reason about.
The Demo-Seed Decision
The dashboard is empty on day one. That's a problem for a portfolio โ
a recruiter opens a link, sees "connect Strava," and bounces before
they see the actual product. So the app ships with a
demo-data seeder: one button drops 12 weeks of
synthetic runs into the database with a deterministic PRNG (so
re-seeds produce the same numbers), a realistic training ramp, and
source='manual' so it never collides with real Strava
data. Clear and re-seed at will.
Small feature, disproportionate impact. It's the difference between a prototype and a demo.
Tradeoffs
- Supabase over a hand-rolled auth stack. Auth + a managed Postgres in one API is worth the vendor lock-in for a personal-scale project. If I wanted to migrate, the schema is plain Postgres and RLS policies are portable.
- Server actions instead of REST routes. The sync, disconnect, and shoe-CRUD flows are all server actions โ co-located with the pages that call them, typed end-to-end, and revalidate the dashboard route automatically.
- Recharts over D3. Two charts, dark theme, needs to look good on the first render. D3 would be overkill; Recharts ships something reasonable in one component per chart.
- 90-day sync window. Balances useful history against Strava's rate limit and the "first-run wait" cost. Long-run me would rather back-fill lazily on demand than block the post-connect UX.
What I Learned
Building on my own data is a cheat code โ every product decision has a forcing function, because I'm the user. The shoe-rotation UI exists because I actually forgot to replace a pair last spring and my knee told me about it for a month. The demo seeder exists because I wanted to show the dashboard to my dad and Strava was rate-limited. When you're the user, the roadmap writes itself.
โ Source on GitHub ยท Next.js 15 ยท Supabase ยท Strava OAuth