API architecture
The API is a NestJS 11 application at apps/api, port 3001. It’s the single source of business truth for the platform. The Next.js web app, the future WhatsApp bot, and the future native client all consume the same REST surface.
Bootstrap entry point: apps/api/src/main.ts. App module: apps/api/src/app/app.module.ts.
Bootstrap pipeline (main.ts)
./instrumentruns first — Sentry is initialized before NestFactory builds the app (required for the Sentry SDK to capture early boot errors).validateEnv()— schema-checks env at startup (skipped underNODE_ENV=test). Source:apps/api/src/config/env.schema.ts.NestFactory.createwithbufferLogs: trueandrawBody: true(Svix webhook signature verification needs the raw body).- Pino logger swapped in via
app.useLogger(app.get(Logger)). - Helmet + compression middleware.
- CORS — origin allow-list from
ALLOWED_ORIGINS, with*.subdomainsuffix matching. Exposessentry-trace,baggage, andx-request-id. - Global ValidationPipe with
whitelist: true, transform: true. - Socket.IO adapter — Redis adapter when
REDIS_URLis set (multi-instance scaling), otherwise default in-memory. - Swagger mounted at
/api/docswith bearer-auth scheme. app.listen(PORT || 3001).
Module organization
Every feature is a NestJS module. The full list, from app.module.ts:
| Module | Role |
|---|---|
DatabaseModule | Global — provides DATABASE_CLIENT (pg Pool + Drizzle). |
AuthModule | Global — provides CLERK_CLIENT (Clerk backend SDK). |
RedisModule | Global — provides REDIS (ioredis client). |
R2Module | Cloudflare R2 client + presigned URL helpers. |
UsersModule | User CRUD, /users/me, profile completion, national-ID encryption. |
OrganizationsModule | Org CRUD, settings. |
MembershipsModule | Membership lifecycle, invitations, role management. |
InvitationsModule (in memberships) | Invitation tokens + Clerk-ticket flow. |
ProgramsModule | Programs (`feed |
ProgramTemplatesModule | Reusable program structures (FIT-74). |
CoursesModule | Course config + entitlements. |
LocationsModule | Per-org physical locations. |
ClassTypesModule | Class type templates. |
ClassSessionsModule | Scheduled session instances, bulk actions. |
BookingsModule | Member bookings, waitlist, check-in. |
ExercisesModule | Exercise library + canonicalization. |
WorkoutsModule | Workouts + workout movements + snapshots. |
WorkoutAssignmentsModule | Coaching-grid assignment cells. |
WorkoutResultsModule | Member result entries + PR detection. |
DailyProgrammingModule | Daily WOD posting. |
PlatformTiersModule | Tier metadata + the PlatformTierGuard and @RequiresFeature decorator. |
PlatformBillingModule | FitKit’s B2B Cardcom billing of orgs. |
PlansModule | Plan SKUs (subscription / class_pack / drop_in / course). |
PaymentsModule | Provider adapters (Cardcom, Morning, iCredit, etc.), transactions, refunds. |
SubscriptionsModule | Member subscription lifecycle (pending → active → past_due → debt → cancelled). |
MessagesModule | 1:1 + workout-thread messaging. |
AnnouncementsModule | Org-wide broadcasts. |
NotificationsModule | In-app + email notifications. |
PushNotificationsModule | Expo push fan-out via BullMQ. |
EventTrackingModule | Server-side PostHog event ingestion. |
TasksModule | Operator to-do queue (lead follow-up, manual refund, cancellation review, etc.). |
WebhooksModule | Inbound webhook receivers (Clerk, payment providers). |
LeadsModule, OrganizationLeadsModule | Lead capture + pipeline. |
BodyMetricsModule, ProgressPhotosModule | Member body data. |
GoalsModule | Member goals (body metric / exercise PR). |
LegalModule | Versioned legal docs + consent. |
FormsModule | Forms engine (compliance + check-in). |
MetricSetsModule | Metric definitions + per-template metric sets. |
EmbeddingsModule, AiModule (Spotter) | Embeddings + agent runtime. |
AnalyticsModule, InsightsModule | Operator dashboards. |
AdminModule | Internal-only admin endpoints + cost dashboards. |
ImportModule, ExportModule | Arbox/CSV imports, CSV exports. |
UploadsModule | Presigned upload endpoints (R2). |
BullBoardModule | Mounts the BullMQ admin UI. |
TestingModule | Test-only. Mounted only when NODE_ENV !== 'production' && TEST_HOOKS_ENABLED === 'true'. Exposes /testing/* endpoints (DB seed, cron triggers). |
Global guards / interceptors / filters
Wired in app.module.ts’s providers:
APP_FILTER AllExceptionsFilter
APP_GUARD AuthGuard (1st — verifies Clerk token, loads DB user)
APP_GUARD PlatformTierGuard (2nd — enforces @RequiresFeature(...))
APP_GUARD ThrottlerGuard (3rd — 60 req / 60s default)
APP_INTERCEPTOR LoggingInterceptor (Pino logging + Sentry scope tagging)
APP_INTERCEPTOR CacheControlInterceptorAuthGuard (apps/api/src/auth/auth.guard.ts)
- Default: protects every route.
- Opt out with
@Public(). - Reads
Authorization: Bearer <token>, verifies with Clerk’sverifyToken, populatesrequest.auth = { userId, sessionId, user }whereuseris the loaded DB row (ornullif Clerk user has no DB row yet). - Test bypass. When
TEST_AUTH_BYPASS=true(non-prod only),x-test-user-id,x-test-session-id,x-test-roleheaders stand in for a real token. Used by E2E tests and integration tests. Disabled in production by construction. - Tags Sentry user on success; adds a Sentry breadcrumb on token verification failure.
PlatformTierGuard (apps/api/src/platform-tiers/platform-tier.guard.ts)
- Inspects
@RequiresFeature(...)metadata. - Resolves the request’s org via
request.params.orgId, looks upplatform_tieronce. - Checks each required feature against
PLATFORM_TIER_MAP(in@fitkit/shared). ThrowsForbiddenExceptionon missing entitlement. - Caches
request.platformTier+request.platformTierConfigfor downstream handlers to use.
ThrottlerGuard
ThrottlerModule.forRoot({ throttlers: [{ ttl: 60000, limit: 60 }] }) — 60 requests per 60s per IP by default. Health endpoint opts out via @SkipThrottle().
LoggingInterceptor (apps/api/src/app/logging.interceptor.ts)
- Captures request id, user id, org id (from
:orgIdparam) into log + Sentry scope. - Logs warn on 4xx, error on 5xx, and warn on >800ms responses tagged
SLOW. - Reflects
requestIdback asX-Request-Idresponse header.
CacheControlInterceptor (apps/api/src/common/interceptors/cache-control.interceptor.ts)
- Only acts on
GETs. - Default
Cache-Control: private, no-cache. Override with@CacheControl('public, max-age=60')or opt out entirely with@CacheControl(null)(which emitsno-store).
AllExceptionsFilter (apps/api/src/app/http-exception.filter.ts)
- Extends Sentry’s
SentryGlobalFilter. - Normalises the response body to
{ ...originalBody, statusCode, requestId, timestamp }.
DI tokens
The two tokens you’ll inject 90% of the time:
| Token | Module | Resolves to | Import path |
|---|---|---|---|
DATABASE_CLIENT | DatabaseModule (global) | Drizzle DbClient from @fitkit/db | apps/api/src/database/database.module.ts |
CLERK_CLIENT | AuthModule (global) | @clerk/backend ClerkClient | apps/api/src/auth/auth.module.ts |
Other commonly-injected tokens:
| Token | Module | Resolves to |
|---|---|---|
REDIS | RedisModule (global) | ioredis client |
@InjectQueue('<name>') | Per-feature queue module | BullMQ Queue instance |
Usage:
constructor(
@Inject(DATABASE_CLIENT) private db: DbClient,
@Inject(CLERK_CLIENT) private clerk: ClerkClient,
) {}Decorators
| Decorator | Defined in | What it does |
|---|---|---|
@Public() | apps/api/src/auth/public.decorator.ts | Skip AuthGuard on this route. |
@CurrentUser('userId' | 'user' | ...) | apps/api/src/auth/current-user.decorator.ts | Inject the request’s auth payload (or a field of it). |
@RequiresFeature(...) | apps/api/src/platform-tiers/requires-feature.decorator.ts | Gate the route on platform-tier features. |
@CacheControl(directive | null) | apps/api/src/common/interceptors/cache-control.interceptor.ts | Override default Cache-Control. |
@SkipThrottle() | NestJS @nestjs/throttler | Opt this route out of the rate limiter. |
Response convention
All responses wrap data:
// Single
{ "data": { "id": "...", "name": "...", "status": "active" } }
// Paginated
{ "data": [ ... ], "meta": { "total": 120, "page": 1, "pageSize": 20 } }Shape contracts are Zod schemas in @fitkit/shared (e.g. UserResponse, ApiResponse<T>, PaginatedResponse<T>).
Explicit state fields, not booleans. Where a status has multiple meaningful values, the response carries the enum value. The web client never re-derives state from inferred booleans — that’s what makes the API consumable by non-web clients without duplication.
Swagger
SwaggerModule.setup('api/docs', app, document) in main.ts. Bearer auth declared via DocumentBuilder().addBearerAuth(). Per-route decoration uses @ApiTags('Feature'), @ApiOperation, @ApiResponse — standard NestJS Swagger conventions.
WebSockets
Single Socket.IO server, multi-instance-safe via the Redis adapter (@socket.io/redis-adapter). Origin is locked to FRONTEND_URL (credentials allowed).
Concrete gateways live inside their feature modules — search for files ending in .gateway.ts. Typical use: 1:1 messaging delivery, typing indicators, announcement fan-out, Spotter streaming.
Background jobs (BullMQ)
BullModule.forRootAsync is wired globally in app.module.ts and connects to REDIS_URL. Each feature module that owns a queue calls BullModule.registerQueue({ name: '...' }) and the matching service processor (@Processor('...') class) handles jobs.
Modules registering queues today (greppable):
apps/api/src/exercises/apps/api/src/ai/embeddings/apps/api/src/programs/apps/api/src/workouts/apps/api/src/export/apps/api/src/import/apps/api/src/push-notifications/apps/api/src/admin/services/admin-queues.service.ts(admin inspection)
The Bull-Board UI is at apps/api/src/bull-board/ and gates access via membership-admin auth.
Cron jobs
NestJS @Cron decorators on services. Master switch: CRONS_ENABLED=true (default false). Without the flag, every @Cron-decorated handler bails immediately in crons-enabled.ts (apps/api/src/common/crons-enabled.ts). This keeps dev DBs from staying warm 24/7 and burning Neon free-tier minutes.
Known cron users: billing retry, pre-charge reminders, no-show sweep, AI storage-snapshot.
In-process event bus
EventEmitterModule.forRoot() is enabled globally. Subsystems emit events, others listen via @OnEvent. Canonical example: Forms onboarding hook — apps/api/src/forms/forms.service.ts listens for membership-activation events and auto-issues compliance forms. Used to break import cycles between subsystems that otherwise would have to import each other.
Webhook security
rawBody: true is set on the Nest app so Svix can verify the Clerk webhook signature against the unparsed body. The Clerk webhook controller (apps/api/src/webhooks/clerk-webhook.controller.ts) verifies svix-id, svix-timestamp, and svix-signature headers with CLERK_WEBHOOK_SECRET before acting on user.created, user.updated, user.deleted events.
Where to read next
auth.md— Clerk verification, user sync, multi-org.database.md— Drizzle schema layout, migrations.observability.md— Pino + Sentry + PostHog wiring details.