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

Deploy

Topology

ComponentHostNotes
API (apps/api)RailwayContainerized via the project’s Nx webpack build.
Web (apps/web)VercelNext.js 16 app; vercel.json at repo root selects the project.
Admin (apps/admin)VercelStandalone Vite app; apps/admin/vercel.json.
Marketing (apps/marketing)Vercel (apps/marketing/vercel.json)Astro site.
Minisites (apps/minisites)VercelAstro multi-tenant; custom domains attached programmatically via Vercel API. See VERCEL_TOKEN, VERCEL_MINISITES_PROJECT_ID.
PostgresManaged (Railway)pgvector extension required.
RedisManagedUsed for BullMQ, Socket.IO adapter, Spotter rate limit.
R2CloudflareTwo buckets: general (R2_BUCKET_NAME) and compliance (R2_COMPLIANCE_BUCKET_NAME, used for FIT-158 signed PDFs with object-lock).

Promotion path

PR (smoke + e2e gates) → main → Railway/Vercel auto-deploy (API runs db:migrate at start) → smoke ↑ CD Full Test Gate validates main on manual dispatch

1. PR opened

ci-smoke-tests.yml runs:

  • DB migration against a fresh test DB.
  • API unit + integration + e2e (skipped for web-only PRs).
  • Web unit + integration (skipped for api-only PRs).
  • Playwright web E2E (separate job, runs once smoke is green).

Required to pass before merge.

2. Merge to main

cd-full-test-gate.yml re-runs the full suite (including Playwright web E2E) against main on manual dispatch. PR checks already ran the same suites against the merge commit, so this is only needed when that assumption breaks: risky deploys, direct pushes to main, or a merge whose checks ran against a stale base. A red run means main is broken and blocks further deploys.

3. Auto-deploy

  • API — Railway watches main and builds the api project. The nx build api output is shipped to a Node container.
  • Web — Vercel watches main and builds apps/web.
  • Admin / minisites / marketing — same pattern, per project.
  • Order: API before web. API and web deploy independently from the same main commit. When a PR changes an API response the web reads, let Railway finish (deployment live, /health 200) before the Vercel build is promoted; if Vercel finishes first, hold its promotion until the API is live. Clients are written to tolerate the reverse window (e.g. holdings fall back to plan.type via holdingKind when kind is missing), so this is belt and braces, not a hard dependency.
  • Release commits are skipped. Each vercel.json sets ignoreCommand so Vercel skips commits whose message starts with chore(shared): release v. That bot commit only bumps libs/shared/package.json, so there is nothing to deploy. A skipped build shows as “Canceled” in Vercel, not as an error.

4. Migrate the database

The Railway deployment runs pnpm db:migrate itself as the first step of the API’s start command, before the new image serves traffic. The deploy log shows nx run @taikan/db:migrate followed by migrations applied successfully. A failing migration fails the deploy and Railway keeps the previous image serving.

That makes two things load-bearing for every migration-carrying PR:

  • A migration must be safe against the data prod already holds. A CHECK or NOT NULL that existing rows violate fails the whole batch. Ship the backfill as its own drizzle-kit custom migration between the column and the constraint, and run any read-only pre-check the PR documents against prod before merging.
  • The journal must stay strictly monotonic (see migrations.md); a stale when makes drizzle report success while applying nothing.

Manual pnpm db:migrate against prod is only for recovery, from a machine with the prod DATABASE_URL, and only with the owner’s approval.

5. Smoke

After Railway/Vercel swap:

  • GET https://<api>/health → 200
  • GET https://app.taikan.fit/en → 200, locale set, Clerk session loads
  • Sign in as a test member → dashboard renders
  • Spotter chat dock opens (coach persona) → first turn returns

If any step fails, see incident-response.md.

6. Post-deploy data backfills

Run any one-off backfill the PR lists, after the API that writes the new data is live, from a machine with the prod DATABASE_URL exported in the shell (backfill scripts do not read .env). Always dry-run first and check the printed Target: host/database, then apply with a manifest kept somewhere durable.

  • memberships.end_date (taikan#586): scripts/backfill-membership-end-date.ts.
    1. DATABASE_URL=… pnpm exec tsx scripts/backfill-membership-end-date.ts. Each row prints its rule (left, join_link_never_paid, join_shell_discarded, invitation_revoked) and current joined_at; per-rule counts follow. Review the never-joined rows: their joined_at becomes NULL.
    2. … --apply --manifest <file>. One transaction, statement_timeout 5min, lock_timeout 10s; a lock timeout aborts with nothing written, just re-run.
    3. A second --apply must report 0 rows.
    4. To undo: … --rollback <file> (dry run), then … --rollback <file> --apply. It restores end_date / joined_at and refuses if any row changed since.

Rollback

WhatHow
APIRailway → previous deployment → “Roll back”.
WebVercel → previous deployment → “Promote to production”.
DBNo down-migrations. Either forward-fix or restore from the managed snapshot (Railway backups). See migrations.md.

Environment variables in prod

Set in Railway (API, marketing) and Vercel (web, admin, minisites) project settings. Never check production keys into the repo or .env.example — the file holds only sandbox/test placeholders.

Key production-only vars:

  • DATABASE_URL (PgBouncer pool) + DIRECT_DATABASE_URL (direct, for migrations)
  • REDIS_URL
  • CLERK_SECRET_KEY (prod instance, sk_live_*)
  • CLERK_WEBHOOK_SECRET (prod webhook endpoint)
  • PAYMENT_CREDENTIALS_ENCRYPTION_KEY, NATIONAL_ID_ENCRYPTION_KEY — see credentials-rotation.md
  • INVITE_SECRET, QR_CHECKIN_SECRET
  • CARDCOM_* real terminal, PLATFORM_BILLING_* Taikan’s own Cardcom credentials, MORNING_* prod hosts
  • R2_* prod bucket credentials, R2_COMPLIANCE_BUCKET_NAME (object-locked)
  • VERCEL_TOKEN, VERCEL_MINISITES_PROJECT_ID — minisite domain provisioning
  • SENTRY_DSN, SENTRY_RELEASE=$RAILWAY_GIT_COMMIT_SHA, SENTRY_AUTH_TOKEN (build-time, for source-map upload)
  • POSTHOG_API_KEY, POSTHOG_PROJECT_ID — server-side event ingestion + admin observability
  • CRONS_ENABLED=true — opt scheduled jobs back in (off by default in .env.example)

Deploy gates

A PR should not be merged if:

  • CI is red.
  • The PR adds a migration but the description doesn’t explain rollback.
  • The PR removes a data-testid that’s referenced in apps/web/e2e/.
  • The PR adds a new env var without updating .env.example.
  • The PR adds a user-facing string without keys in all three of en.json, he.json, ru.json.

These are conventions, not enforced gates — code review is the gatekeeper today.