Skip to Content
Living documentation — last reviewed 2026-05-28
ArchitectureArchitecture overview

Architecture overview

A bird’s-eye view of how Taikan 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/).

taikan/ ├── 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 (Vercel deploy) │ ├── minisites/ Astro per-org public minisite renderer │ └── docs/ Nextra-rendered docs site (this tree, served on port 3030) ├── libs/ │ ├── db/ @taikan/db — Drizzle schema + client │ └── shared/ @taikan/shared — Zod schemas + TS types shared API↔web↔mobile ├── docs/ this tree (canonical markdown source for apps/docs) └── scripts/ dev/test scripts, enrichment one-offs

The native member app (taikan-mobile) lives in a separate repo sibling on disk (~/dev/taikan-mobile/), not under apps/. It consumes @taikan/shared as a versioned GitHub Packages dependency. See mobile.md for the rationale (toolchain divergence, EAS pipeline, native artifact churn).

Apps at a glance

AppPortPurposeStackDeploy
apps/api3001All business logic. REST + Socket.IO. Swagger at /api/docs.NestJS 11, webpack-built, Node 22Railway (apps/api/railway.toml)
apps/web3000Operator dashboard + member surfaces.Next.js 16, React 19, Tailwind v4, shadcn/uiVercel (vercel.json)
apps/admin(Vite dev)Taikan-internal admin tools.Vite + React + ClerkVercel (internal)
apps/marketingn/aPublic marketing site.AstroVercel (apps/marketing/vercel.json)
apps/minisitesn/aPer-org public minisites.AstroVercel; Vercel Domain API for custom domains
apps/docs3030This docs site.Nextra v4, Next.js 16GitHub Pages (.github/workflows/deploy-docs.yml)
taikan-mobile—Native member app (iOS + Android).Expo SDK 55, React Native 0.81, NativeWind 4, Clerk ExpoEAS Build + Submit (separate repo)

Libs at a glance

LibPath aliasPurpose
libs/db@taikan/dbDrizzle ORM schema and createDbClient. All tables, enums, and types.
libs/shared@taikan/sharedZod 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/taikan-shared package on GitHub Packages.

Path aliases come from tsconfig.base.json.

Apps depend on libs, never on each other

An app must not import another app’s source, including from a spec file or a one-off tool. Nx turns a single relative import such as ../../../web/src/... into a project-graph edge, and build depends on ^build, so an api -> web edge makes the API image build run a full next build first. Code two apps need goes in libs/shared.

pnpm check:boundaries (scripts/check-project-boundaries.mjs) enforces this from the Nx project graph and runs in the typecheck job of ci-smoke-tests.yml. It also rejects a negated implicitDependencies entry ("!@taikan/web"), which would hide such an edge from the graph without removing the import. The root ESLint rule @nx/enforce-module-boundaries flags the same imports, but lint is not a CI gate.


Request flow (clients → API)

The API is the single source of business truth. Three first-class clients today; more planned.

Browser taikan-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, taikan-mobile ⇄ Socket.IO ⇄ apps/api (NestJS WS gateways) │ ▼ Redis (pub/sub) │ ▼ Other apps/api instances

apps/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):

ModuleQueue 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.tsAdmin 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 envUse
R2_BUCKET_NAMEGeneral attachments, exports, uploads.
R2_COMPLIANCE_BUCKET_NAMESigned 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

ToolScopeWired in
SentryAPI + Web + Marketing + Minisitesapps/api/src/instrument.ts, Next.js Sentry SDK on web
PostHogFrontend events + server-side payment funnel + Spotter cost telemetryapps/web/src/providers/posthog-provider.tsx, apps/api/src/event-tracking/
Pino logsAPI structured loggingapps/api/src/app/app.module.ts (LoggerModule.forRoot) + LoggingInterceptor
Bull-BoardJob queuesapps/api/src/bull-board/
Health endpointDB + Redis livenessapps/api/src/app/health.controller.ts (GET /health)

See architecture/observability.md for the deep dive (sample rates, beforeSend filters, audit-log gap).


Deployment

AppHostNotes
apps/apiRailwayapps/api/railway.toml.
apps/webVercelvercel.json at repo root. Next.js framework preset; automatic Git deployments only from main.
apps/marketingVercelapps/marketing/vercel.json. Automatic Git deployments only from main.
apps/adminVercelapps/admin/vercel.json. Internal-only; automatic Git deployments only from main.
apps/minisitesVercelapps/minisites/vercel.json. Automatic Git deployments only from main; custom domains attach via Vercel Domain API (VERCEL_TOKEN, VERCEL_MINISITES_PROJECT_ID).

PostgreSQL and Redis in production are managed (Railway Postgres; 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.


  • Sales workspace — organization-scoped capability, lead work queue, atomic commitments, inbox and reporting; ADR-0023.

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