Skip to main content

deployment overview

A Psychic application runs multiple process roles from a single built image. Each role has its own entrypoint:

RoleEntrypointPurpose
webnode ./dist/src/main.jsHTTP API server
websocketnode ./dist/src/ws.jsWebSocket server (if using @rvoh/psychic-websockets)
workernode ./dist/src/worker.jsBackground job processor (if using @rvoh/psychic-workers)
console / migratornode ./dist/src/conf/system/cli.js db:migrateDatabase migrations and CLI tasks
warning

Do not rely on pnpm (or any package manager runner) in production containers. Use direct node ./dist/... commands in your deployment configuration (task definitions, Procfiles, Docker CMD, etc.).

Infrastructure requirements

At minimum, you need:

  • a Postgres database
  • a server instance running Node.js 20.9.0 or later
  • a load balancer
  • a way to provision and rotate secrets securely (e.g. AWS Secrets Manager)

If using websockets or background workers:

  • a Redis 7.2.0+ instance

If using background workers:

  • worker instances running Node.js

Environment variables

Always use AppEnv (defined in src/conf/AppEnv.ts) to access environment variables — never process.env directly. AppEnv validates that required variables are present at boot time and provides a clear error when one is missing. This also decouples the application from the container environment: secrets can be loaded from AWS Secrets Manager or AWS SSM Parameter Store at boot time and injected into AppEnv before anything else runs.

For variables that are only present in some environments, use AppEnv.string(name, { optional: true }), which returns string | undefined instead of throwing.

Build output

If the project uses TypeScript path aliases (e.g. @app/*), the production build must run tsc-alias after tsc. Without this step, compiled JS will contain unresolved path alias imports and fail at runtime.

TLS

TLS behind a reverse proxy

If your reverse proxy (load balancer, ingress controller) terminates TLS and re-encrypts traffic to the container, the Psychic app must speak TLS on its container port. Baking a self-signed certificate into the image is more reliable than generating one dynamically at runtime.

If the proxy terminates TLS and forwards plain HTTP to the container, no additional TLS configuration is needed in the app.

Postgres TLS

Configure TLS posture explicitly in conf/dream.ts via the ssl field on each credential. The bare useSsl boolean is deprecated. The scaffolded boilerplate uses verified TLS by default:

const dbSsl: { rejectUnauthorized: true } | false = AppEnv.boolean('DB_NO_SSL')
? false
: { rejectUnauthorized: true }

Provider reference:

ProviderRequired ssl value
Supabase, Neon, Render, Azure Database for PostgreSQL Flexible Server{ rejectUnauthorized: true } (default)
AWS RDS{ rejectUnauthorized: true, ca: readFileSync('rds-ca.pem') }
GCP Cloud SQL{ rejectUnauthorized: true, ca: readFileSync('server-ca.pem') }
Heroku Hobby, some local Docker images{ rejectUnauthorized: false }
Local dev with no TLSssl: false or DB_NO_SSL=true

Health checks

Register an explicit health check route in conf/routes.ts:

r.get('health_check', HealthCheckController, 'index')

The @rvoh/psychic-websockets package has a built-in health check, but note two important differences from the API health check:

APIWebSocket
Default path/health_check (you define it)/healthcheck (built-in)
HTTP methodsResponds to GET and HEADResponds to GET only

curl -I (which sends HEAD) works against the API health check but returns 404 from the WebSocket service. When verifying WebSocket health externally, use curl -X GET instead.

Running migrations in production

The migrate task is the boot smoke test

Running node ./dist/src/conf/system/cli.js db:migrate exercises the full Psychic boot chain before applying any migrations:

  1. Node boot on the deployed image.
  2. loadEnv resolves the environment, including any external-secret injection wired through AppEnv.
  3. AppEnv materializes — every required env var is validated; the application halts with a clear error if anything is missing.
  4. The Psychic registry initializes — every module that registers on import (models, serializers, controllers, routes, workers, websocket channels) runs its top-level side effects.
  5. A database connection is established using the configured credentials, including TLS negotiation.

Only then does Kysely start applying pending migrations. Any failure in steps 1–5 fails the migrate task before the schema changes — meaning a separate container "boot smoke" task is redundant. The migrate task itself is the smoke test, and it covers strictly more: missing env vars, registry-time import errors, malformed DB credentials, TLS handshake failures, and network reachability all surface here.

Combine db:migrate and db:seed in one invocation

Migrate and seed share the same image, execution role, env, and DB credentials. On serverless container platforms where each one-off task pays a fixed scheduling and image-pull overhead, invoking them as two separate tasks pays the per-task lifecycle twice for no reason. Combine them in a single container invocation:

sh -c "node ./dist/src/conf/system/cli.js db:migrate && node ./dist/src/conf/system/cli.js db:seed"

If migrate fails, seed never runs and the task exits non-zero. If migrate succeeds and seed fails, the task exits non-zero with seed's error.

Debugging a deployed service

When a deployed Psychic service is not responding correctly:

  1. Check the public endpoint — is it returning the expected status code?
  2. Check health check status — is the target healthy from the load balancer/ingress perspective?
  3. Check orchestrator events — container restarts, OOM kills, failed deployments.
  4. Check the running configuration — correct image, correct command, correct environment variables, correct log destination.
  5. Check application logs — look for startup errors, missing env vars, failed migrations.
  6. Verify HTTP method — if health checks pass but manual verification fails, confirm you're using GET (not HEAD) for WebSocket endpoints.
tip

See the AWS guide for a complete example of provisioning a Psychic application on AWS Fargate.