Skip to Content
Living documentation — last reviewed 2026-05-28
RunbooksLocal environment setup

Local environment setup

End-to-end checklist for a new engineer (or agent) to get FitKit running on a fresh machine.

Prerequisites

  • Node 20.x (matches @types/node@20.19.9)
  • pnpm 10.29+ — corepack enable && corepack prepare pnpm@10.29.1 --activate
  • Docker + docker compose (Postgres + Redis run in containers)
  • A Clerk dev instance (free tier is enough)
  • Cloudflare R2 bucket for file storage (optional in pure local; required to exercise uploads, exports, forms PDFs)

1. Clone & install

git clone git@github.com:desmotech/fitkit.git cd fitkit pnpm install

2. Infrastructure (Postgres + Redis)

Two stacks live in docker-compose.yml:

ServicePortPurpose
postgres5432Dev DB (fitkit_dev). pgvector extension preinstalled (used by the embeddings module — libs/db/src/lib/schema/_pgvector.ts).
redis6379BullMQ, Socket.IO adapter, Spotter rate limit / cache
make dev-up # postgres + redis make dev-db-migrate

The test stack lives in docker-compose.test.yml and runs Postgres on port 55432 to avoid colliding with dev. See testing/strategy.md.

3. Backend env (apps/api/.env)

Copy .env.example and fill the [REQUIRED] entries. Minimum to boot:

  • DATABASE_URL — defaults to local docker.
  • REDIS_URL — defaults to local docker.
  • CLERK_SECRET_KEY — from Clerk dashboard → API Keys.
  • FRONTEND_URL=http://localhost:3000
  • ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002
  • PAYMENT_CREDENTIALS_ENCRYPTION_KEY — 32-byte hex. Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  • NATIONAL_ID_ENCRYPTION_KEY — same.
  • CLERK_WEBHOOK_SECRET — only needed if you wire up Clerk webhooks locally (use ngrok).

Do not commit apps/api/.env. It’s gitignored.

Optional but commonly needed

VarWhy you’d set it
R2_*File uploads, exports, forms PDFs. Without them the upload pipeline degrades.
RESEND_API_KEYOutbound email. re_test_dummy works for unit/integration tests; for real send-test you need a real key.
CRONS_ENABLED=trueEnable scheduled jobs (billing retry, no-show sweeps, AI snapshots). Default off so dev DB stays cold.
CARDCOM_*, MORNING_*, RIVHIT_*, ICREDIT_*Payments providers. Sandbox URLs in .env.example.
VOYAGE_API_KEYExercise embedding enrichment. Without it, similarity search returns empty.
ANTHROPIC_API_KEYLLM enrichment scripts + Spotter agent. Without it the agent endpoints will fail.
SENTRY_DSNError tracking. Leave empty to disable.

4. Frontend env (apps/web/.env.local)

Minimum:

  • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY — pair to the secret key above.
  • NEXT_PUBLIC_API_URL=http://localhost:3001
  • NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
  • NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

Optional: NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_GOOGLE_MAPS_API_KEY.

5. Admin env (apps/admin/.env.local) — only if developing on the admin app

  • VITE_API_URL=http://localhost:3001
  • VITE_CLERK_PUBLISHABLE_KEY=...

6. Boot

pnpm dev # API on 3001, web on 3000 (concurrently) # or one at a time: pnpm dev:api pnpm dev:web

Smoke-test:

7. Seed data (optional)

pnpm seed:movements # canonical movement library pnpm enrich:aliases # only if VOYAGE_API_KEY + ANTHROPIC_API_KEY are set

End-to-end seed for E2E tests is handled by testApi.seedX() helpers under apps/web/e2e/. See testing/strategy.md.

Troubleshooting

SymptomCauseFix
relation "..." does not exist on API bootDB not migratedmake dev-db-migrate
CORS: origin not allowedWeb origin missing from ALLOWED_ORIGINSAdd http://localhost:3000 (and admin if used)
too many clients from pgPool size mismatched to DB planSee DB_POOL_MAX notes in .env.example
Clerk session not persisted on the webMismatched publishable/secret keys, or middleware running on a non-locale pathConfirm both .env files reference the same Clerk instance
Forms PDF render fails locallyPuppeteer can’t find ChromiumSet PUPPETEER_EXECUTABLE_PATH to your local Chrome (/Applications/Google Chrome.app/Contents/MacOS/Google Chrome on macOS)
_journal.json when complaintStale migration after mergeRegenerate the stale migration so its when exceeds the latest applied. See migrations.md.