Developer Setup
import { Aside, Steps } from ‘@astrojs/starlight/components’;
Prerequisites
Section titled “Prerequisites”- Node.js 18 or later
- Git
- A Supabase project (free tier works)
- A Cloudflare R2 bucket (free tier works)
OAuth integrations (Wahoo, Strava) require registered developer apps. You can run FitRepo without them in limited mode — the settings page will simply show those integrations as unavailable.
Clone and install
Section titled “Clone and install”git clone https://github.com/simonker/fitrepo.gitcd fitreponpm installConfigure environment variables
Section titled “Configure environment variables”Copy the example file and fill in your credentials:
cp .env.example .env.localOpen .env.local and set the following. The comments in .env.example explain where to find each value.
Required (app won’t start without these)
Section titled “Required (app won’t start without these)”| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase → Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase → Settings → API → anon key |
SUPABASE_SERVICE_ROLE_KEY | Supabase → Settings → API → service_role key |
Required for workout sync (Wahoo)
Section titled “Required for workout sync (Wahoo)”| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_WAHOO_CLIENT_ID | developers.wahooligan.com |
WAHOO_CLIENT_ID | Same as above |
WAHOO_CLIENT_SECRET | Same app, keep secret |
Register http://localhost:3000/api/auth/wahoo/callback as an allowed redirect URI in your Wahoo app settings.
Required for FIT file storage (R2)
Section titled “Required for FIT file storage (R2)”| Variable | Where to find it |
|---|---|
R2_ENDPOINT | https://<account_id>.r2.cloudflarestorage.com |
R2_BUCKET_NAME | Your bucket name (e.g. fitrepo-dev) |
R2_ACCESS_KEY_ID | Cloudflare dashboard → R2 → API Tokens |
R2_SECRET_ACCESS_KEY | Same token |
Optional
Section titled “Optional”| Variable | Purpose |
|---|---|
NEXT_PUBLIC_STRAVA_CLIENT_ID / STRAVA_CLIENT_ID / STRAVA_CLIENT_SECRET | Strava destination integration |
RESEND_API_KEY | Transactional emails (sync failures, import completion). Skipped if absent. |
NEXT_PUBLIC_APP_URL | Base URL for email links. Defaults to https://mydatafor.life. Set to http://localhost:3000 locally. |
NEXT_PUBLIC_SENTRY_DSN | Sentry error tracking. Skipped if absent. |
AXIOM_TOKEN + AXIOM_DATASET | Axiom log forwarding via next-axiom. Skipped if absent. |
Set up the database
Section titled “Set up the database”-
Bootstrap the base schema
The incremental migrations assume the core tables already exist. Run the baseline schema first:
supabase/schema.sql -
Run migrations in order
Then run each migration file in the Supabase SQL editor in chronological order:
supabase/migrations/20260524_expand_activities.sqlsupabase/migrations/20260524_rate_limits.sqlsupabase/migrations/20260530_activity_edit_fields.sqlsupabase/migrations/20260530_backfill_jobs.sqlsupabase/migrations/20260531_activities_updated_at.sqlsupabase/migrations/20260601_mcp_api_keys.sqlsupabase/migrations/20260601_oauth_codes.sqlsupabase/migrations/20260601_r2_status.sql -
Verify
Check that the following tables exist in the Table Editor:
activities,outbound_sync,user_connections,user_settings,user_thresholds,wellness,athlete_profile,rate_limits,backfill_jobs,mcp_api_keys,oauth_codes
Start the dev server
Section titled “Start the dev server”npm run devOpen http://localhost:3000. Sign up with an email address — Supabase handles auth.
Run tests
Section titled “Run tests”npm test # run oncenpm run test:watch # watch modeCore business logic is unit-tested with Vitest. Tests live in src/lib/__tests__/ and use mocked Supabase clients — no live DB required. Newer route-level flows (backfill jobs, MCP OAuth, activity edit) have partial or no test coverage.
Project layout
Section titled “Project layout”src/├── app/│ ├── (app)/ # Authenticated pages (dashboard, activities, settings…)│ ├── (auth)/ # Login, signup, reset-password│ └── api/ # API routes (webhooks, MCP, backfill, OAuth)├── lib/ # Business logic — all unit tested│ ├── processWebhook.ts # Core activity ingestion pipeline│ ├── wahoo.ts # Wahoo API pagination│ ├── destinations.ts # Strava + Intervals.icu upload│ ├── fitnessModel.ts # CTL/ATL/TSB calculations│ ├── oauth.ts # MCP OAuth 2.0 + PKCE helpers│ ├── email.ts # Resend transactional emails│ ├── r2.ts # Cloudflare R2 FIT file storage│ └── mcp-auth.ts # MCP Bearer token authentication├── components/│ ├── layout/ # AppNav, SiteFooter│ └── ui/ # Primitive components (button, card, input…)└── types/ └── database.ts # Supabase table typesDeployment
Section titled “Deployment”The main app is deployed to Vercel manually — it is not auto-deployed from GitHub pushes.
vercel --prodThe docs site (docs/) auto-deploys on push to main when files under docs/ change.
Key gotchas
Section titled “Key gotchas”createClient()vscreateServiceClient()—createClient()respects Row Level Security (use in pages).createServiceClient()bypasses RLS (use only in API routes andprocessWebhook.ts).- Never change the INSERT to UPSERT in
processWebhook.ts— the(activity_id, destination)unique constraint + plain INSERT is the double-push prevention mechanism. - OAuth Connect buttons are
<a>tags — OAuth URLs are server-computed. Do not replace them withonClickhandlers. - Supabase
.single()returns{ data: null, error }on zero rows — always checkdatabefore using it. Buttonhas noasChildprop — FitRepo uses@base-ui/react. For a button-styled link usebuttonVariants()asclassNameon a plain<a>.