CH Charles Hayes
← back to all projects

WHOOP Personal Analytics Dashboard

Self-hosted WHOOP data dashboard with OAuth integration, hourly auto-sync via cron, and ~365-day backfill on first sign-in.

Next.js 15 TypeScript Drizzle ORM libSQL/Turso Auth.js Vercel Cron

Problem

I swim around 10K yards a week and I’m building toward a first sprint triathlon, which means swim, bike, run and lifting all drawing down the same recovery budget. That’s the question the WHOOP app is worst at — not “how did I sleep last night” but “is this load sustainable across the next 90 days.”

The app surfaces day-level views well but is weak for long-horizon questions: is my recovery trending up over the last 90 days? What does sleep look like across travel weeks vs home weeks? I wanted a private dashboard I could shape to my own questions.

Approach

A Next.js 15 app that authenticates via WHOOP OAuth, backfills ~365 days of data on first sign-in, then keeps itself current via an hourly Vercel Cron job. Data is stored in Turso (libSQL) with Drizzle ORM for schema and migrations. Single-user from the start — no multi-tenancy, no admin pages, no anonymized sharing.

Architecture

  • Auth: Auth.js with the WHOOP OAuth provider. Token refresh handled in a server action; refresh failures degrade gracefully into a “reconnect” prompt rather than a 500.
  • Data: Drizzle schema covers recoveries, sleeps, workouts, cycles. libSQL/Turso gives me a hosted SQLite I can query with edge latency.
  • Sync: Vercel Cron hits a protected route hourly. Initial backfill is a one-shot job behind the same route, gated by a “have we ever synced” check.
  • Frontend: Server components for the heavy aggregations (recovery trends, sleep stage stack), client components only where interactivity demands it (date-range picker).

Outcomes / What I learned

  • Building for one user is liberating — no auth scopes to design, no rate-limit budgets to apportion, no analytics to instrument. The whole codebase is smaller than it looks.
  • Vercel Cron + a hosted SQLite is shockingly low-overhead for a personal data product. The whole thing fits inside the Vercel/Turso free tiers.
  • WHOOP’s OAuth refresh story is brittle in practice; designing for “the refresh failed, ask the user to reconnect” up front saved a lot of pager noise later.