Skip to content

Developer Setup

import { Aside, Steps } from ‘@astrojs/starlight/components’;

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.


Terminal window
git clone https://github.com/simonker/fitrepo.git
cd fitrepo
npm install

Copy the example file and fill in your credentials:

Terminal window
cp .env.example .env.local

Open .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)”
VariableWhere to find it
NEXT_PUBLIC_SUPABASE_URLSupabase → Settings → API → Project URL
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase → Settings → API → anon key
SUPABASE_SERVICE_ROLE_KEYSupabase → Settings → API → service_role key
VariableWhere to find it
NEXT_PUBLIC_WAHOO_CLIENT_IDdevelopers.wahooligan.com
WAHOO_CLIENT_IDSame as above
WAHOO_CLIENT_SECRETSame app, keep secret

Register http://localhost:3000/api/auth/wahoo/callback as an allowed redirect URI in your Wahoo app settings.

VariableWhere to find it
R2_ENDPOINThttps://<account_id>.r2.cloudflarestorage.com
R2_BUCKET_NAMEYour bucket name (e.g. fitrepo-dev)
R2_ACCESS_KEY_IDCloudflare dashboard → R2 → API Tokens
R2_SECRET_ACCESS_KEYSame token
VariablePurpose
NEXT_PUBLIC_STRAVA_CLIENT_ID / STRAVA_CLIENT_ID / STRAVA_CLIENT_SECRETStrava destination integration
RESEND_API_KEYTransactional emails (sync failures, import completion). Skipped if absent.
NEXT_PUBLIC_APP_URLBase URL for email links. Defaults to https://mydatafor.life. Set to http://localhost:3000 locally.
NEXT_PUBLIC_SENTRY_DSNSentry error tracking. Skipped if absent.
AXIOM_TOKEN + AXIOM_DATASETAxiom log forwarding via next-axiom. Skipped if absent.

  1. Bootstrap the base schema

    The incremental migrations assume the core tables already exist. Run the baseline schema first:

    supabase/schema.sql
  2. Run migrations in order

    Then run each migration file in the Supabase SQL editor in chronological order:

    supabase/migrations/20260524_expand_activities.sql
    supabase/migrations/20260524_rate_limits.sql
    supabase/migrations/20260530_activity_edit_fields.sql
    supabase/migrations/20260530_backfill_jobs.sql
    supabase/migrations/20260531_activities_updated_at.sql
    supabase/migrations/20260601_mcp_api_keys.sql
    supabase/migrations/20260601_oauth_codes.sql
    supabase/migrations/20260601_r2_status.sql
  3. 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


Terminal window
npm run dev

Open http://localhost:3000. Sign up with an email address — Supabase handles auth.


Terminal window
npm test # run once
npm run test:watch # watch mode

Core 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.


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 types

The main app is deployed to Vercel manually — it is not auto-deployed from GitHub pushes.

Terminal window
vercel --prod

The docs site (docs/) auto-deploys on push to main when files under docs/ change.


  • createClient() vs createServiceClient()createClient() respects Row Level Security (use in pages). createServiceClient() bypasses RLS (use only in API routes and processWebhook.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 with onClick handlers.
  • Supabase .single() returns { data: null, error } on zero rows — always check data before using it.
  • Button has no asChild prop — FitRepo uses @base-ui/react. For a button-styled link use buttonVariants() as className on a plain <a>.