This guide defines the production deployment order for the Trust-Link backend. Use it for staging and production releases so infrastructure, database migrations, service rollout, and validation happen in a consistent sequence.
- Node.js 22 runtime (the version pinned by
.nvmrcand both Docker stages). - PostgreSQL 16 (the version used by Docker Compose and CI).
- Redis for response and tracking cache when
REDIS_URLis configured. - Stellar Horizon access for the selected network.
- SendGrid and Twilio credentials when notifications are enabled.
- HTTPS termination at the load balancer or ingress.
- Centralized log collection for JSON logs.
- OpenTelemetry collector when tracing is enabled.
Set all required variables before running migrations or starting the service:
NODE_ENV=productionPORTDATABASE_URLSEP10_JWT_SECRETADMIN_ADDRESSSTELLAR_NETWORKSTELLAR_WEBHOOK_SECRETREDIS_URLALLOWED_ORIGINSAPI_BASE_URLSENDGRID_API_KEYTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENOTEL_ENABLEDOTEL_EXPORTER_OTLP_ENDPOINTCREDENTIAL_ENCRYPTION_KEY(64-character hex string for logistics API key encryption)
Keep secrets in the deployment platform secret manager. Do not bake them into images, workflow files, or migration scripts.
Swagger UI is intentionally disabled when NODE_ENV=production. Production
operators and API consumers should obtain the generated schema from the CI
OpenAPI artifact or run npm run openapi:generate against the release source.
The docker-compose.yml includes a production profile optimized for production deployments. To use the production profile:
-
Create a
.envfile with production secrets:SEP10_JWT_SECRET=your-production-jwt-secret-at-least-32-chars ADMIN_ADDRESS=your-admin-stellar-address POSTGRES_PASSWORD=your-secure-postgres-password CREDENTIAL_ENCRYPTION_KEY=64-character-hex-string-for-encryption OTEL_ENABLED=false OTEL_EXPORTER_OTLP_ENDPOINT=
-
Ensure the
CREDENTIAL_ENCRYPTION_KEYis exactly 64 hex characters (32 bytes). Generate one with:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Start with production profile
docker-compose --profile production up -d
# View logs
docker-compose --profile production logs -f
# Stop services
docker-compose --profile production down- Restart Policy:
restart: alwaysfor automatic recovery - Resource Limits: CPU and memory limits for each service
- Logging: JSON-file driver with log rotation (10MB max, 3 files)
- Health Checks: All services include health checks with proper dependency ordering
- No Development Mounts: Source code volume mounts removed for security
- Non-root User: Application runs as
nestjsuser (UID 1001)
- PostgreSQL: Uses
pg_isreadyto verify database connectivity - Redis: Uses
redis-cli pingto verify Redis is responding - Application: Uses three HTTP endpoints — pick the right one for each job:
| Endpoint | Semantics | When to use |
|---|---|---|
GET /health/live |
Liveness | Kubernetes/container-orchestrator livenessProbe. Always returns 200 if the HTTP stack is up; NO dependency checks. A failure here means "restart the container". Never triggers a restart because an upstream dependency is briefly down. |
GET /health/ready |
Readiness | Kubernetes/container-orchestrator readinessProbe and load-balancer target-group health. Runs full dependency checks (PostgreSQL + Horizon, Redis reported but optional). Returns 503 when required dependencies are unreachable, so the instance is removed from rotation temporarily. |
GET /health |
Legacy | Alias for /health/ready (readiness semantics). Preserved for backwards compatibility with existing monitors; new deployments should prefer the two endpoints above. |
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3Load balancers (AWS ALB, GCP LB, nginx, etc.) should poll /health/ready (not /health/live) so an instance with a database outage is taken out of the pool instead of returning 5xx to real users.
The application service waits for both PostgreSQL and Redis to be healthy before starting, preventing crash-loops due to unavailable dependencies.
Production services have the following resource limits:
- app-prod: 1 CPU, 1GB memory (reserve: 0.5 CPU, 512MB)
- db-prod: 1 CPU, 1GB memory (reserve: 0.5 CPU, 512MB)
- redis-prod: 0.5 CPU, 512MB memory (reserve: 0.25 CPU, 256MB)
- Confirm the target
DATABASE_URLpoints at the production database. - Take a database backup and record the backup identifier in the release notes.
- Run
npm ciin a clean build environment. - Run
npm run db:generate. - Apply migrations with
npm run db:migrateor the platform migration job. - Verify Prisma can connect with a read-only health query.
- Start one application instance against the migrated database.
- Verify health, auth, escrow reads, admin reads, webhooks, and notification queues.
- Roll out remaining instances after validation passes.
Never run application instances from a new build against an old schema when the release includes required schema changes.
- Install dependencies:
npm ci. - Type-check:
npm run typecheck. - Lint:
npm run lint:check. - Unit tests with coverage:
npm run test:cov. - Coverage gate:
node scripts/check_coverage.js. - Build:
npm run build. - Build container image:
npm run docker:build. - Run database migration job.
- Deploy one canary instance.
- Promote to full rollout after validation milestones pass.
- The service starts without configuration warnings for required production variables.
GET /health/livereturns 200 (always, no dependencies touched).GET /health/readyreturns 200 once database + Horizon are reachable; returns 503 when required dependencies are unavailable.GET /health(legacy alias) returns the same status code and body asGET /health/ready.GET /versionreturns the expected release version.- SEP-10 challenge and verify flows issue tokens.
- Vendor escrow list queries return within the expected latency budget.
- Admin endpoints return 403 for vendor tokens and 200 for admin tokens.
PATCH /admin/dispute/:id/resolveis reachable only by admin JWTs.- Webhook signature validation rejects missing or invalid signatures.
- Queue dashboard and logs show no failed background jobs.
- Error rate and p95 latency remain stable for at least one canary window.
The 20260526000000_initial baseline migration creates the foundational tables (Escrow, VendorProfile, Dispute, Notification) and enums. All statements are idempotent (IF NOT EXISTS), making them safe on any database.
If your production database was originally set up via prisma db push (no _prisma_migrations table), you must run this one-time transition before the baseline is applied:
bash scripts/resolve-existing-migrations.shThis script syncs the schema and marks all 14 existing migrations as applied, so subsequent prisma migrate deploy runs only apply new changes.
If your production database already uses Prisma Migrate (has _prisma_migrations records), no action is needed — the baseline migration is a no-op via IF NOT EXISTS.
The .github/workflows/db-migrate.yml workflow applies Prisma migrations automatically.
Triggers:
- Runs on every push to
main - Runs on pull requests (status check)
- Supports manual dispatch with a
dry_runoption via the GitHub Actions UI
Dry run (preview without applying):
- Go to Actions → Database Migrations → Run workflow
- Set
dry_runtotrue - The workflow runs
prisma migrate statusto show pending migrations without applying them
Migration status check:
After every run (apply or dry run), prisma migrate status is executed so the log confirms which migrations were applied and the schema is in sync.
Running migrations before integration / E2E tests:
The test.yml workflow already runs npx prisma migrate deploy before executing tests. The dedicated db-migrate.yml workflow handles production deployments and previews independently.
- Stop the rollout and keep the canary isolated.
- Revert the application image to the previous release.
- Restore the database from the pre-release backup when migrations are not backward-compatible.
- Re-run health, auth, escrow, admin, and webhook validation.
- Document the failing milestone before reopening rollout.
Migration rollback procedure:
Prisma does not support automatic down migrations. To roll back a schema change:
# 1. Restore from the pre-release database backup
pg_restore -U trustlink -d trustlink_prod backup_pre_release.dump
# 2. Revert to the previous application image and restart
# 3. Verify the application connects and passes health checks
# To mark a failed migration as rolled back in Prisma's migration table:
npx prisma migrate resolve --rolled-back <migration_name>Always take a labelled database snapshot before applying migrations to a shared environment.