Skip to main content
Version: 0.9.14

Database credentials

Delphi uses two Postgres roles on purpose: a migration credential for schema and setup work, and a runtime credential for day-to-day application traffic. Splitting them lets you rotate the runtime password through AWS Secrets Manager without giving every long-running container superuser access.

At a glance

RoleVariableWho consumes itTypical privileges
MigrationMIGRATION_DATABASE_URLOps voiceai-db-migrate one-shot only (init.sh migrations, delphi-setup.sh)DDL — CREATE/ALTER/DROP, enum changes, registry seed steps
RuntimeDATABASE_URLWeb, API, Voice, Tasker, Scaler (all long-running services)Application CRUD on the platform schema

MIGRATION_DATABASE_URL

Scope: Ops service only, db-migrate container in vars.yaml. Never inject this into Web, API, Voice, Tasker, or Scaler.

Connection: Direct Postgres (not PgBouncer). Migrations need session-level DDL and enum operations that poolers often block or mishandle.

Used for:

  • Release migrations during ./init.sh or docker compose --profile migration run --rm db-migrate
  • Platform setup via ./delphi-setup.sh / ./delphi-setup.sh --apply-missing

Credential source:

DeploymentTypical migration user
Self-managed Postgres on the Database hostOwner / superuser (often the same as POSTGRES_USER on that host)
RDS / AuroraMaster user or a dedicated migration DB user with DDL rights

Store MIGRATION_DATABASE_URL in the Ops Secrets Manager bundle (or compose it from a migration-only secret). It is independent of the runtime app user.

See Database migrations in init and update and Ops → Database migrations.

DATABASE_URL

Scope: Every service that runs Prisma against the platform database at runtime.

Connection: Production paths usually go through PgBouncer with TLS — see Internal encryption. The runtime user should have read/write on application tables only, not superuser.

Credential source:

  • Self-managed: per-service Secrets Manager bundle or composed URL — see Managed database secrets for the RDS/Aurora pattern.
  • Bootstrap + fetch-env.sh rebuilds the URL on each ./update.sh --restart-only.

Long-running services must not reuse MIGRATION_DATABASE_URL as their DATABASE_URL.

Runtime rotation — DATABASE_SECRET_ARN

For day-2 password rotation without redeploying every container on each AWS rotation event, TypeScript services that use @delphi/db support an optional runtime refresh path:

VariablePurpose
DATABASE_SECRET_ARNSecrets Manager secret holding the runtime user credentials
DATABASE_SECRET_REGIONRegion for the secret (defaults to AWS_REGION)
DATABASE_SECRET_MODEauto, plaintext, json, or rds-json — how to parse the secret payload

When set, the shared database client refetches the secret after authentication failures, validates a new pool with SELECT 1, and swaps it in — so AWS automatic rotation of the runtime user can propagate without a full platform restart.

Important:

  • This applies to runtime credentials only. The migration container reads MIGRATION_DATABASE_URL once at job start and does not use DATABASE_SECRET_ARN.
  • Other env vars (JWT keys, provider API keys) still require a container restart unless their owning client implements a similar refresh hook.

Ops Tasker and Scaler declare DATABASE_SECRET_ARN in their vars.yaml; wire the same pattern on API, Web, and Voice when your deployment enables AWS rotation for the app DB user.

  1. Provision two users in Postgres (or two Secrets Manager secrets): migration (DDL) and runtime (CRUD).
  2. Set MIGRATION_DATABASE_URL on Ops to the migration user — rotate this secret rarely, only when the migration role password changes.
  3. Set DATABASE_URL on app services to the runtime user (directly or via compose.template).
  4. Optionally set DATABASE_SECRET_ARN on long-running Node services to the runtime secret so AWS rotation updates pools without a coordinated restart.
  5. On platform upgrade: run db-migrate with the migration URL before rolling Web/API/Voice to the new tag.

For generic secret-store steps (stage, restart set, verify, rollback), see Rotate secrets. For RDS master-user composition, see Managed database secrets.

Common mistakes

MistakeRisk
Same superuser in DATABASE_URL and MIGRATION_DATABASE_URL on all hostsRuntime compromise exposes DDL/superuser
Running db-migrate with the runtime CRUD user onlyMigrations fail on enum/table changes
Putting MIGRATION_DATABASE_URL on Tasker/API/VoicePrivileged URL in long-lived containers
Rotating runtime password in SM but not in Postgres (or vice versa)Auth failures until both sides match; use dual-credential window
Expecting DATABASE_SECRET_ARN to refresh migration jobsMigrations still need explicit MIGRATION_DATABASE_URL update + re-run

See also