Architecture overview
A bird’s-eye view of how FitKit is structured. Each section below either has a dedicated deep-dive (api.md, web.md, database.md, etc.) or links into the code.
Monorepo layout
Nx workspace, pnpm package manager. Two distinct kinds of project: applications (apps/) and libraries (libs/).
fitkit/
├── apps/
│ ├── api/ NestJS backend (port 3001)
│ ├── web/ Next.js 16 customer + dashboard app (port 3000)
│ ├── admin/ Vite + React internal-only admin console
│ ├── marketing/ Astro marketing site (separate Railway deploy)
│ ├── minisites/ Astro per-org public minisite renderer
│ └── docs/ Nextra-rendered docs site (this tree, served on port 3030)
├── libs/
│ ├── db/ @fitkit/db — Drizzle schema + client
│ └── shared/ @fitkit/shared — Zod schemas + TS types shared API↔web↔mobile
├── docs/ this tree (canonical markdown source for apps/docs)
└── scripts/ dev/test scripts, enrichment one-offsThe native member app (fitkit-mobile) lives in a separate repo sibling on disk (~/dev/fitkit-mobile/), not under apps/. It consumes @fitkit/shared as a versioned GitHub Packages dependency. See mobile.md for the rationale (toolchain divergence, EAS pipeline, native artifact churn).
Apps at a glance
| App | Port | Purpose | Stack | Deploy |
|---|---|---|---|---|
apps/api | 3001 | All business logic. REST + Socket.IO. Swagger at /api/docs. | NestJS 11, webpack-built, Node 22 | Railway (apps/api/railway.toml) |
apps/web | 3000 | Operator dashboard + member surfaces. | Next.js 16, React 19, Tailwind v4, shadcn/ui | Vercel (vercel.json) |
apps/admin | (Vite dev) | FitKit-internal admin tools. | Vite + React + Clerk | Vercel (internal) |
apps/marketing | n/a | Public marketing site. | Astro | Railway (apps/marketing/railway.toml) |
apps/minisites | n/a | Per-org public minisites. | Astro | Vercel; Vercel Domain API for custom domains |
apps/docs | 3030 | This docs site. | Nextra v4, Next.js 16 | GitHub Pages (.github/workflows/deploy-docs.yml) |
fitkit-mobile | — | Native member app (iOS + Android). | Expo SDK 55, React Native 0.81, NativeWind 4, Clerk Expo | EAS Build + Submit (separate repo) |
Libs at a glance
| Lib | Path alias | Purpose |
|---|---|---|
libs/db | @fitkit/db | Drizzle ORM schema and createDbClient. All tables, enums, and types. |
libs/shared | @fitkit/shared | Zod schemas + TS types shared between API, web, and mobile (response shapes, entity DTOs, platform-tier feature map, locale dictionaries). Mobile consumes it as the published @desmotech/fitkit-shared package on GitHub Packages. |
Path aliases come from tsconfig.base.json.
Request flow (clients → API)
The API is the single source of business truth. Three first-class clients today; more planned.
Browser fitkit-mobile (iOS/Android)
│ │
▼ HTTPS ▼ HTTPS
Next.js (Vercel) Expo Router app
│ • Middleware │ • Clerk Expo session
│ (apps/web/src/ │ (token cache in
│ middleware.ts): │ expo-secure-store)
│ - locale detection │ • useApi() hook attaches
│ - Clerk auth on │ Clerk bearer + X-Locale
│ protected routes │ • 10s JWT cache + single
│ - legacy dashboard │ 401 retry
│ path redirects │
│ Server components: │
│ - serverFetch(path) │
│ attaches bearer │
│ Client components: │
│ - useApi() hook │
│ attaches bearer │
▼ ▼
NestJS API (Railway)
│
│ Global pipeline:
│ 1. Helmet, CORS, compression
│ 2. ValidationPipe (whitelist + transform)
│ 3. AuthGuard (verifies Clerk JWT, loads DB user)
│ 4. PlatformTierGuard (org-tier feature gates)
│ 5. ThrottlerGuard (60 req / 60s default)
│ 6. LoggingInterceptor (Pino + Sentry scope)
│ 7. CacheControlInterceptor
│ 8. AllExceptionsFilter (Sentry + structured error body)
▼
│
│ Global pipeline:
│ 1. Helmet, CORS, compression
│ 2. ValidationPipe (whitelist + transform)
│ 3. AuthGuard (verifies Clerk JWT, loads DB user)
│ 4. PlatformTierGuard (org-tier feature gates)
│ 5. ThrottlerGuard (60 req / 60s default)
│ 6. LoggingInterceptor (Pino + Sentry scope)
│ 7. CacheControlInterceptor
│ 8. AllExceptionsFilter (Sentry + structured error body)
▼
PostgreSQL (Drizzle ORM)Public endpoints opt out of auth with @Public(). Health check (/health) and Clerk webhook (/webhooks/clerk) are the canonical examples. See architecture/api.md for the full pipeline.
Real-time / WebSocket flow
Browser, fitkit-mobile ⇄ Socket.IO ⇄ apps/api (NestJS WS gateways)
│
▼
Redis (pub/sub)
│
▼
Other apps/api instancesapps/api/src/main.ts wires the Socket.IO Redis adapter when REDIS_URL is set. Without Redis the server falls back to the in-process adapter — single-instance only. Used for messaging, typing indicators, presence, and live update fan-out.
Mobile uses socket.io-client with transports: ['websocket'] only and AppState-driven reconnect (foreground → connect, background → disconnect). See mobile.md for the lifecycle details.
Background jobs (BullMQ)
Producers (services in apps/api/src/**) ─→ BullMQ queues ─→ Redis ─→ Workers (in-process)Queues registered today (greppable via BullModule.registerQueue):
| Module | Queue role |
|---|---|
apps/api/src/exercises/ | Admin canonical exercise jobs |
apps/api/src/ai/embeddings/ | Exercise / workout / program / member-profile embeddings (Voyage API) |
apps/api/src/programs/ | Program-level async work |
apps/api/src/workouts/ | Workout-level async work |
apps/api/src/export/ | CSV / report exports |
apps/api/src/import/ | Arbox / CSV imports (multi-phase) |
apps/api/src/push-notifications/ | Expo push fan-out |
apps/api/src/admin/services/admin-queues.service.ts | Admin queue inspection |
Bull-Board UI (apps/api/src/bull-board/) exposes queue state internally.
In-process event bus
NestJS EventEmitterModule.forRoot() is enabled globally in app.module.ts. Subsystems emit domain events (e.g. membership.activated) and other modules subscribe via @OnEvent. Canonical example: the Forms module’s onboarding hook (apps/api/src/forms/forms.service.ts Onboarding auto-issue block) listens for membership activation and auto-issues configured compliance forms.
This is intentionally separate from BullMQ — events are in-process, synchronous within the worker, no persistence. Use BullMQ when you need retries, scheduling, or cross-process delivery.
File storage
Cloudflare R2. Two logical buckets:
| Bucket env | Use |
|---|---|
R2_BUCKET_NAME | General attachments, exports, uploads. |
R2_COMPLIANCE_BUCKET_NAME | Signed compliance PDFs (FIT-158). 7+ year retention, immutability, no-delete — configure lifecycle + object-lock at bucket level. Falls back to R2_BUCKET_NAME when unset (dev only). |
Module: apps/api/src/r2/. All reads presign a fresh URL (1h default) — no public URLs are served.
Observability
| Tool | Scope | Wired in |
|---|---|---|
| Sentry | API + Web + Marketing + Minisites | apps/api/src/instrument.ts, Next.js Sentry SDK on web |
| PostHog | Frontend events + server-side payment funnel + Spotter cost telemetry | apps/web/src/providers/posthog-provider.tsx, apps/api/src/event-tracking/ |
| Pino logs | API structured logging | apps/api/src/app/app.module.ts (LoggerModule.forRoot) + LoggingInterceptor |
| Bull-Board | Job queues | apps/api/src/bull-board/ |
| Health endpoint | DB + Redis liveness | apps/api/src/app/health.controller.ts (GET /health) |
See architecture/observability.md for the deep dive (sample rates, beforeSend filters, audit-log gap).
Deployment
| App | Host | Notes |
|---|---|---|
apps/api | Railway | apps/api/railway.toml. PR previews via .github/workflows/deploy-pr-preview.yml — vercel[bot] preview URL triggers a Railway preview environment deploy. |
apps/web | Vercel | vercel.json at repo root. Next.js framework preset. |
apps/marketing | Railway | apps/marketing/railway.toml. Astro adapter. |
apps/admin | Vercel | apps/admin/vercel.json. Internal-only. |
apps/minisites | Vercel | apps/minisites/vercel.json. Custom domains attached via Vercel Domain API (VERCEL_TOKEN, VERCEL_MINISITES_PROJECT_ID). |
PostgreSQL and Redis in production are managed (Neon for DB is the documented default; Railway managed Redis). Local dev uses docker-compose.yml — Postgres on 5432 (pgvector image), Redis on 6379. Test stack uses docker-compose.test.yml — Postgres on 55432, Redis on 56379.
Where to read next
api.md— NestJS modules, global pipeline, DI tokens.web.md— Next.js routing, providers, data helpers, feature gating.database.md— schema domains, migration policy, encryption.auth.md— Clerk on both sides, multi-org, webhooks.i18n.md— locale model, RTL, dictionary discipline.observability.md— Sentry/PostHog/Pino + audit-log gap.infra.md— hosting, env vars, CI workflows, secrets.