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-offsThe 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
| 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) | Taikan-internal admin tools. | Vite + React + Clerk | Vercel (internal) |
apps/marketing | n/a | Public marketing site. | Astro | Vercel (apps/marketing/vercel.json) |
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) |
taikan-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 | @taikan/db | Drizzle ORM schema and createDbClient. All tables, enums, and types. |
libs/shared | @taikan/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/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 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. |
apps/web | Vercel | vercel.json at repo root. Next.js framework preset; automatic Git deployments only from main. |
apps/marketing | Vercel | apps/marketing/vercel.json. Automatic Git deployments only from main. |
apps/admin | Vercel | apps/admin/vercel.json. Internal-only; automatic Git deployments only from main. |
apps/minisites | Vercel | apps/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.
Where to read next
-
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.