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
The WHOOP 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 my 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.