Infrastructure¶
1. Environments¶
| Environment | Branch | Database | Managed by |
|---|---|---|---|
| Local | any | Docker Compose Postgres | Developer |
| Development | dev |
Railway Postgres (auto-deploy) | Railway |
| Staging | stage |
Railway Postgres (auto-deploy) | Railway |
| Production | main |
Railway Postgres (auto-deploy) | Railway |
All Railway environments use the same railway.toml config. The only differences between them are environment variables set in the Railway dashboard (e.g. DATABASE_URL, NODE_ENV).
Local setup details: chapter 00
2. Prisma Workflow¶
Two commands, two purposes¶
| Command | When | What it does | Creates migration files? | Safe for production? |
|---|---|---|---|---|
prisma migrate dev |
Local development | Diffs schema against DB, generates a new migration SQL file, applies it, runs prisma generate |
Yes | No — interactive, may reset data |
prisma migrate deploy |
CI / Railway | Applies existing migration files in order, never creates new ones, never prompts | No | Yes — deterministic |
Creating a migration (local only)¶
- Edit
prisma/schema.prisma - Run
npx prisma migrate dev --name descriptive-name - Prisma generates a SQL file in
prisma/migrations/<timestamp>_<name>/migration.sql - The migration is applied to your local DB
prisma generateruns automatically, updating the client insrc/generated/prisma/- Commit the migration file — it's the source of truth for schema changes
Migration files are immutable¶
Once a migration SQL file is committed, never edit it. If a migration is wrong:
- Create a new migration that fixes it
- Or, if it's only on your local branch and hasn't been merged: npx prisma migrate reset to wipe and start over
Pre-commit migrations are editable¶
The immutability rule above applies only to committed migrations. An uncommitted migration folder under prisma/migrations/ — still untracked in git status — is fair game. If a follow-up schema edit logically belongs to the same unit of change, fold it into the existing uncommitted migration instead of stacking a new one.
The full fold procedure (delete folder → migrate reset → re-run migrate dev) and the deploy-safety audit every new migration must pass live in chapter 12. Read chapter 12 before running npx prisma migrate dev.
Prisma config¶
prisma.config.ts defines schema location, migration path, and reads DATABASE_URL from .env. Both migrate dev and migrate deploy use this config.
schema.prisma has no url in the datasource block — it's provided at runtime by the config file (local) or by Railway's environment variable (deployed).
3. Railway Deployment Pipeline¶
Configured in railway.toml. All three environments (dev/staging/prod) follow the same pipeline:
What happens on git push¶
git push origin dev
│
▼
┌─────────────────────────────────────────┐
│ 1. BUILD │
│ Builder: Railpack │
│ - npm ci (automatic) │
│ - npx prisma generate │
│ - nest build │
│ Output: dist/ folder │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. PRE-DEPLOY │
│ npx prisma migrate deploy │
│ && npx prisma db seed │
│ && npm run db:reconcile-rbac │
│ - Reads DATABASE_URL from Railway │
│ - Applies pending migration files │
│ - Seeds Tier 1 in every environment │
│ - Reconciles every preset role │
│ across global + tenant rows │
│ - If this fails → deploy aborts, │
│ old version keeps running │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. START │
│ node dist/src/main.js │
│ - Reads env vars from Railway │
│ - Connects to Railway Postgres │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. HEALTH CHECK │
│ GET /api/v1/health/ready │
│ - Must return 200 within 300s │
│ - Pings DB with SELECT 1 │
│ - If it fails → deploy rolls back │
└─────────────────────────────────────────┘
4. Branch → Environment Mapping¶
Set up in Railway dashboard (not in code):
- Push to
dev→ deploys to development environment - Push to
stage→ deploys to staging environment - Push to
main→ deploys to production environment
5. Failure Modes¶
| Failure point | What happens |
|---|---|
| Build fails (TS errors, missing deps) | Deploy aborted, nothing changes |
prisma migrate deploy fails |
Deploy aborted, old version keeps running, DB unchanged |
prisma db seed fails |
Deploy aborted before preset reconciliation/start |
db:reconcile-rbac fails verification |
Transaction rolls back; deploy aborts and the old version keeps running |
| Health check times out (300s) | Deploy rolled back to previous version |
| App crashes after deploy | Restarted up to 3 times (ON_FAILURE policy), then stops |
6. Environment Variables¶
Each environment needs these set in Railway:
| Variable | Required | Source / Default | Notes |
|---|---|---|---|
DATABASE_URL |
Yes | Railway Postgres (${{Postgres.DATABASE_URL}}) |
Auto-managed by Railway. Superuser/owner connection — migrations, seed, AdminPrismaService (bypasses RLS). |
APP_DATABASE_URL |
Yes on hosted (boot-throws otherwise) | Manual | Runtime connection as the non-superuser app_user role — the connection RLS applies to (ch02 §4). Rollout order per environment: (1) psql "$DATABASE_URL" -f prisma/sql/provision-app-role.sql, (2) set a strong password on app_user, (3) set this var, (4) deploy. On production-like NODE_ENV a missing value fails env validation at boot, and a value resolving to a superuser/BYPASSRLS role fails the onModuleInit role probe — the silent superuser fallback exists only on local/test. |
JWT_SECRET |
Yes | Manual | Token signing key — unique per environment |
JWT_EXPIRATION |
Yes | Manual | Access token TTL (e.g. 15m) |
NODE_ENV |
No | Manual | development / staging / production |
PORT |
No | Railway | Usually auto-set by Railway |
CORS_ORIGIN |
No | Manual | Comma-separated allowed origins |
COOKIE_DOMAIN |
No | Manual | Cookie scope domain |
LOG_LEVEL |
No | Manual | trace / debug / info / warn / error / fatal |
REFRESH_TOKEN_EXPIRATION_DAYS |
No | Default: 7 |
Refresh token TTL in days |
PASSWORD_RESET_URL_BASE |
Yes on hosted | Local: http://localhost:5173/reset-password |
Trusted frontend reset page. Must be an absolute HTTPS URL on hosted environments; never supplied by the public request. |
PASSWORD_RESET_TOKEN_TTL_MINUTES |
No | Default: 30 |
Reset-token lifetime from sender claim; bounded to 5–1440 minutes. |
PASSWORD_RESET_SWEEP_INTERVAL_MS |
No | Default: 30000 (0 in test) |
Durable sender cadence; 0 disables only the timer, not manual sweep(). |
PASSWORD_RESET_SENDING_STALE_MS |
No | Default: 600000 |
Crash-reclaim lease window, bounded to 1–60 minutes. |
SWAGGER_USER |
No | Manual | Basic Auth username for Swagger UI (dev/staging only) |
SWAGGER_PASSWORD |
No | Manual | Basic Auth password for Swagger UI (dev/staging only) |
SEED_ADMIN_PASSWORD |
Yes on hosted | Manual (secret store) | Password for the seeded admin accounts. The seed throws on any NODE_ENV ∉ {local,test} if unset — it refuses to author a known default password. Local/test use built-in fallbacks. |
Validated at boot time by src/config/env.validation.ts — missing required vars cause startup failure.
Password recovery additionally requires published Resend template aliases
password-reset-en and password-reset-it. Both templates accept exactly
recipientName, schoolName, resetUrl, and expiresInMinutes.
Security invariants enforced at boot / seed time¶
These are not in env.validation.ts; they fail-fast elsewhere (closed audit finding R5 — see the Wave-1 spec docs/superpowers/specs/2026-06-25-wave1-security-hardening-design.md):
SEED_ADMIN_PASSWORDis required on every hosted env.resolveHostedSeedPassword(prisma/seed/helpers/seed-admin-password.ts) throws when it is unset on anyNODE_ENV ∉ {local,test}, so a hosted deploy that forgot to set it fails at seed time rather than silently provisioning a known-password admin. Set it in the dev/stage secret store before the next deploy.CORS_ORIGIN=*is forbidden on production. With cookie auth, a wildcard origin disablesOriginCheckGuard's CSRF defence. The guard throws at construction onNODE_ENV=productionifCORS_ORIGINcontains*, so the app refuses to boot. Use an explicit comma-separated allow-list on prod. The guard itself is enforced only on production — local/test/development/stage skip the CSRF origin check entirely (a blocked login on a non-production env is pure friction), so a wildcard there is harmless to the guard.CORS_ORIGINis still required on all hosted envs for the CORS response side (env.validation.ts).
7. Typical Workflow: Schema Change¶
Read chapter 12 first. It contains the pre-commit fold rule and the deploy-safety hazard checklist every new migration must pass. The steps below assume both have been applied.
- Local: Edit
schema.prisma, runnpx prisma migrate dev --name add-users-table - Commit:
git add prisma/migrations/ prisma/schema.prisma - Push to
dev: Railway runsprisma migrate deploy→ applies the new migration against dev DB - PR to
stage: Same migration runs against staging DB on merge - PR to
main: Same migration runs against production DB on merge
The migration SQL file is identical in every environment. The only difference is which DATABASE_URL it runs against.
8. railway.toml Reference¶
[build]
builder = "RAILPACK"
buildCommand = "npx prisma generate && npm run build"
[deploy]
startCommand = "node dist/src/main.js"
preDeployCommand = "npx prisma migrate deploy && npx prisma db seed && npm run db:reconcile-rbac"
healthcheckPath = "/api/v1/health/ready"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3
This file is checked into the repo and applies to all Railway environments. The pre-deploy command is uniform across every environment: migrate deploy → db seed → db:reconcile-rbac, with no production override. The seed self-gates on NODE_ENV (production creates no fixture/tenant data); the final privileged transaction uses DATABASE_URL to align and verify every recognized global and tenant preset role before the revision can start. See chapter 15. Environment-specific configuration lives in the Railway dashboard, not here.
9. CI Pipeline¶
GitHub Actions CI (.github/workflows/ci.yml) runs on every pull request (any base branch) and on pushes to stage. Direct pushes to dev / main therefore skip CI — they reach Railway via merged PRs.
Pipeline steps¶
- npm ci — install dependencies
- npm audit — security audit (
--audit-level=moderate) - prisma generate — generate Prisma client
- migrate deploy + migrate diff — apply migrations to the CI DB, then diff the live datasource against
schema.prisma(--from-config-datasource --to-schema, exit 2 = drift) to catch a schema edit with no migration. Raw-SQL-only constraints (partial indexes, CHECKs) are invisible here and are guarded separately bytest/db-constraints.e2e-spec.ts(audit R2) - npm run lint — ESLint
- npm run build — TypeScript compilation
- npm test — unit tests (Jest)
- prisma migrate deploy — apply migrations to CI Postgres
- prisma db seed — seed test data
- npm run test:e2e — end-to-end tests (Supertest)
Branch strategy¶
| Branch | CI | Deploy |
|---|---|---|
dev |
None | Railway auto-deploy to development |
stage |
Full pipeline (audit → lint → build → unit → e2e) | Railway auto-deploy to staging |
main |
None | Railway auto-deploy to production |
The dev and main branches rely solely on Railway's deploy pipeline (build → pre-deploy → start → health check). Full CI validation gates the stage branch before code is promoted to main.