Build, Test & Deploy
Building
Each workspace has its own build script; the root can fan out to all of them.
# Build everything that has a build script
npm run build # root — runs build --workspaces --if-present
# Or build a single app
npm run build --workspace=apps/api # prisma generate + tsc
npm run build --workspace=apps/web # tsc + vite build → dist/
npm run build --workspace=apps/admin # tsc + vite build → dist/
npm run build --workspace=apps/marketing # next buildAlways verify the production bundle
Per the project's UI rules, npm --workspace=apps/web run build must succeed before a page is considered done — the production bundle is what users get, not the dev server.
Testing
The stack is Vitest everywhere (never Jest), @testing-library/react for components, supertest for API routes, vitest-mock-extended for the Prisma mock.
# Run a workspace's suite
npm --workspace=apps/api run test
npm --workspace=apps/web run test
npm --workspace=apps/admin run test
# Watch mode
npm --workspace=apps/api run test:watch
# Coverage (thresholds enforced)
npm --workspace=apps/api run test:coverageCoverage thresholds (build blockers)
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 70% |
| Statements | 80% |
Tests are written in the same task as the implementation. Each service function has at least 3 tests (happy path, validation error, business-rule violation); each API route has at least 3 (401 unauthenticated, 403 wrong role, 422 invalid body). All external dependencies — Prisma, MSG91, Firebase, R2, Redis — are mocked.
Prisma mock pattern
// apps/api/src/lib/__mocks__/prisma.ts
import { mockDeep, mockReset } from 'vitest-mock-extended'
import { PrismaClient } from '@prisma/client'
export const prismaMock = mockDeep<PrismaClient>()
vi.mock('../prisma', () => ({ prisma: prismaMock }))
beforeEach(() => { mockReset(prismaMock) })Linting & type-checking
npm run lint # all workspaces
npm --workspace=apps/api run lint # eslint src --ext .ts
npm --workspace=apps/marketing run typecheck # tsc --noEmitDeployment (Railway)
For the full picture — per-service build config, the migration/
DIRECT_URLsetup, and step-by-step provisioning of every external service (Supabase, Upstash, Cloudflare R2, MSG91, Firebase, Meilisearch, Sentry) — see Deployment (Railway) and External Services & Provisioning.
The API, web and admin apps deploy to Railway; configuration is entirely via environment variables (never hard-coded).
- Build command: Railway uses
npm ci. If a dependency change driftspackage-lock.json, re-sync withnpm install --package-lock-onlyand validate withnpm ci --dry-runbefore pushing. - API image:
node:22-alpine(musl). TheDockerfilesmoke-testsrequire('sharp')afternpm ciso a pruned platform binary fails the build, not production boot. The Dockerfile's workspace-manifest COPY list must name every workspace in the rootpackage.json. - Frontend env:
VITE_*vars are baked at build time from the Railway service env. - Sentry release: set
SENTRY_RELEASE/VITE_RELEASEto the git SHA in CI so error reports link to the deployed build.
Pre-deploy checklist (abridged)
[ ] npm audit --audit-level=high passes (fix high/critical first)
[ ] No secrets in git history
[ ] All new routes have Zod schemas + ownership checks
[ ] Coverage thresholds met
[ ] File upload tested with a non-image file (must 400)
[ ] Env vars set in Railway, not in code
[ ] Health check green on staging: GET /api/v1/healthSee the Security Checklist and the Pre-launch Ops Checklist for the full lists.
Health & monitoring
- Health probe:
GET /api/v1/healthdeep-checks Postgres + Redis and returns503when a configured dependency is down (Better Uptime polls this every minute). - Job dashboard: Bull Board at
/admin/queues(admin auth) shows the SMS + scheduled queues. - API docs: Swagger UI at
/api/docs, spec JSON at/api/docs.json.
