Case Study ยท Personal Data

stride

A personal dashboard for my running data โ€” pulls in workouts from Strava, surfaces trends over weeks and months, and recommends gear when the numbers say I actually need it.

Role Solo build
Type Personal data project
Status Working prototype ยท 2026
Stack Next.js 15 ยท Supabase ยท Strava API ยท Recharts

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

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