Infrastructure¶
This chapter describes runtime configuration, deployment phases and database boundaries. Local setup is in getting started; migration safety is in migrations.
1. Configuration ownership¶
Local services run through Docker Compose. Hosted services use Railway and the shared railway.toml. Branch-to-environment bindings, secrets, databases and storage buckets are configured in the hosting dashboard; the repository does not enforce those bindings.
NODE_ENV controls application behavior independently of the Git branch. Use the values in src/common/constants/environments.ts and the validation in src/config/env.validation.ts, rather than inventing environment names.
The application and administrative database connections have different responsibilities:
| Connection | Responsibility |
|---|---|
APP_DATABASE_URL |
Runtime queries through a restricted PostgreSQL role with RLS enforced |
DATABASE_URL |
Migration/seed/reconciliation and named privileged operations through AdminPrismaService |
Pre-deploy provisions the runtime role (npm run db:provision-app-role) on every hosted deploy, so a fresh database such as a PR preview environment's needs no manual step. The role takes its password from APP_DATABASE_URL (or APP_DB_PASSWORD), so set that URL with a strong password before the first deploy. Outside local/test, the script fails rather than fall back to its default password. The application checks role privileges at startup. See multitenancy for tenant GUCs and privileged access rules.
2. Prisma workflow¶
prisma.config.ts owns the schema path, migrations directory, seed entry point and administrative datasource URL.
| Command | Purpose |
|---|---|
npx prisma migrate dev --name <name> |
Create/apply a development migration; local development only |
npx prisma migrate deploy |
Apply the existing migration history to a target database |
npx prisma generate |
Generate the client from the schema |
npx prisma db seed |
Run the environment-aware seed |
Client generation is an explicit step with this Prisma version; do not assume migrate dev generates it. Read chapter 12 before generating a migration. Audit the SQL before applying it to retained data. Migration history must reproduce the schema, including constraints Prisma cannot express.
Deployment does not make destructive SQL safe. Rehearse data transformations on a representative copy, consider old/new runtime compatibility, and make recovery instructions specific to the database change. Application rollback does not undo committed migrations.
3. Deployment phases¶
The checked-in Railway configuration is:
[build]
builder = "RAILPACK"
buildCommand = "npx prisma generate && npm run build"
[deploy]
startCommand = "node dist/src/main.js"
preDeployCommand = "npm run db:provision-app-role && npx prisma migrate deploy && npm run db:provision-app-role && npx prisma db seed && npm run db:reconcile-rbac"
healthcheckPath = "/api/v1/health/ready"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3
npm run build includes the backoffice dependency installation/build, Prisma client generation and Nest compilation; package.json is the command authority. Railpack selects the Node version from package.json engines.node.
Pre-deploy runs role provisioning, migration, provisioning again, seed and permission reconciliation in sequence. Migrations GRANT … TO app_user, so the role must exist before migrate deploy. The second provisioning pass grants on the tables the migrations created and revokes runtime access to the private credential tables again. Seed behavior depends on NODE_ENV; production creates catalogue prerequisites without fixture tenants. Reconciliation uses the privileged connection to align recognized global and tenant preset roles with the code matrix. See seeding.
| Failure | Operational consequence |
|---|---|
| Build | No pre-deploy migration from this build has run |
| Role provisioning | Its transaction rolls back; a failure after migrate deploy leaves the applied migrations committed |
| Migration | Earlier migrations may already be committed; inspect the failed migration and ledger |
| Seed | Successful migrations remain; seed work may have committed |
| Reconciliation | Its transaction rolls back, but prior migration/seed commits remain |
| Readiness | Runtime replacement/fallback does not restore the database |
| Process crash | Railway applies the configured restart policy |
The readiness endpoint checks database reachability. It does not certify data migration correctness, provider configuration or schema compatibility with an older binary.
Changes that cannot coexist with old writers require an explicit maintenance/cutover procedure: stop incompatible application replicas and jobs, prevent automatic restarts, apply the data change, and keep the stop in place through subsequent failures. Recovery must use a compatible forward fix or an explicitly planned restore.
4. Environment variables¶
.env.template is the local starting point. src/config/env.validation.ts is authoritative for accepted values and bounds; individual configuration modules own defaults.
| Group | Variables and contract |
|---|---|
| Database | DATABASE_URL required; APP_DATABASE_URL required in production-like environments |
| JWT | JWT_SECRET, JWT_EXPIRATION required; TTL is positive, whole-second and at most one hour |
| Refresh | REFRESH_TOKEN_EXPIRATION_DAYS controls refresh lifetime |
| HTTP | PORT, COOKIE_DOMAIN, LOG_LEVEL; explicit CORS_ORIGIN required in production-like environments |
| Storage | S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY required |
| Storage options | S3_FORCE_PATH_STYLE, S3_AUTO_CREATE_BUCKET, S3_SIGNED_URL_TTL_SECONDS |
MAIL_TRANSPORT=log\|resend; Resend requires RESEND_API_KEY, RESEND_WEBHOOK_SECRET, MAIL_FROM |
|
| Notifications | NOTIFICATION_TRANSPORT selects observability/test capture, not whether user delivery occurs |
| Push | PUSH_TRANSPORT=log\|fcm\|memory; FCM requires valid FIREBASE_SERVICE_ACCOUNT_BASE64 |
| Recovery | PASSWORD_RESET_URL_BASE, branding/language and reset token/sender timing variables |
| Invitations | INVITATION_TOKEN_TTL_DAYS, sender interval and stale-claim window |
| Scheduled work | Attendance, notification milestone and communications timing variables; REPORT_WORKER_ENABLED |
| Solver | TIMETABLE_SOLVER_URL, TIMETABLE_SOLVER_SECRET, optional whole-call time limit |
| Swagger | SWAGGER_USER and SWAGGER_PASSWORD must be supplied together |
| Seed | SEED_ADMIN_PASSWORD required outside local/test when seed resolves an admin password |
Memory notification/push transports are test-only in production-like environments. Hosted push must be explicitly configured. Report-worker enablement is separate from API availability.
A signed storage URL remains usable until its expiry; choose the bounded TTL deliberately. Automatic bucket creation is intended for local MinIO. Hosted buckets and credentials are provisioned separately. Match the local S3 endpoint to the actual MinIO listener and Docker port mapping.
Changing JWT configuration does not alter tokens already issued: each keeps its signed expiry.
Mail template contracts¶
Resend delivery depends on published template aliases and variable names:
- Recovery:
password-reset-en/password-reset-it, withrecipientName,schoolName,resetUrl,expiresInMinutes. Branding is account-wide, not selected from a school. - Invitations:
invitation-credentials-en/invitation-credentials-it;src/invitations/invitation-template.tsowns the variable contract.
Keep provider templates synchronized with producer changes. MAIL_FROM accepts a bare address or a display-name mailbox. Per-message display names retain the configured sending address.
5. HTTP security and documentation¶
OriginCheckGuard enforces the production origin policy. A production wildcard CORS_ORIGIN fails startup. Native requests without Origin/Referer require a non-empty X-SIS-Client and no Sec-Fetch-Site; a headerless request does not automatically bypass the check.
CORS response policy and origin enforcement are separate mechanisms. See src/main.ts and src/common/guards/origin-check.guard.ts.
Swagger/Scalar availability is controlled in main.ts: stage and production do not expose it. Basic-auth credentials protect the enabled non-local documentation paths when configured. Do not infer documentation exposure from the branch name.
6. CI¶
.github/workflows/ci.yml runs on every pull request and pushes to stage. This trigger is independent of hosted environment bindings. Direct pushes to other branches do not trigger this workflow.
The backend job installs dependencies, audits packages, generates Prisma, applies migrations and checks schema drift, then runs lint, build, unit and E2E suites. E2E global setup provisions seeded worker database clones; those are separate from the base database used by the migration-drift step. A second job installs and tests the Python solver.
Schema diff does not cover every raw-SQL constraint; database constraint tests cover those contracts separately. The workflow reads the Node version from package.json engines.node (node-version-file), the same field Railpack resolves, so change the version there only. Keep commands aligned across package.json, workflow definitions and hosting configuration rather than treating a successful readiness probe as equivalent to CI.
Testing explains the harness and worker isolation. Agents run checks only when authorized by the repository instructions and user.