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
| Role | Variable | Who consumes it | Typical privileges |
|---|---|---|---|
| Migration | MIGRATION_DATABASE_URL | Ops voiceai-db-migrate one-shot only (init.sh migrations, delphi-setup.sh) | DDL — CREATE/ALTER/DROP, enum changes, registry seed steps |
| Runtime | DATABASE_URL | Web, 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.shordocker compose --profile migration run --rm db-migrate - Platform setup via
./delphi-setup.sh/./delphi-setup.sh --apply-missing
Credential source:
| Deployment | Typical migration user |
|---|---|
| Self-managed Postgres on the Database host | Owner / superuser (often the same as POSTGRES_USER on that host) |
| RDS / Aurora | Master 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.shrebuilds 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:
| Variable | Purpose |
|---|---|
DATABASE_SECRET_ARN | Secrets Manager secret holding the runtime user credentials |
DATABASE_SECRET_REGION | Region for the secret (defaults to AWS_REGION) |
DATABASE_SECRET_MODE | auto, 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_URLonce at job start and does not useDATABASE_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.
Rotation workflow (recommended)
- Provision two users in Postgres (or two Secrets Manager secrets): migration (DDL) and runtime (CRUD).
- Set
MIGRATION_DATABASE_URLon Ops to the migration user — rotate this secret rarely, only when the migration role password changes. - Set
DATABASE_URLon app services to the runtime user (directly or viacompose.template). - Optionally set
DATABASE_SECRET_ARNon long-running Node services to the runtime secret so AWS rotation updates pools without a coordinated restart. - On platform upgrade: run
db-migratewith 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
| Mistake | Risk |
|---|---|
Same superuser in DATABASE_URL and MIGRATION_DATABASE_URL on all hosts | Runtime compromise exposes DDL/superuser |
Running db-migrate with the runtime CRUD user only | Migrations fail on enum/table changes |
Putting MIGRATION_DATABASE_URL on Tasker/API/Voice | Privileged 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 jobs | Migrations still need explicit MIGRATION_DATABASE_URL update + re-run |
See also
- Environment variable index —
MIGRATION_DATABASE_URL,DATABASE_URL,DATABASE_SECRET_ARN - Managed database secrets — RDS/Aurora
secret.arn_from+DATABASE_URLcomposition - Database operations — self-managed Postgres, backups, TLS
- Internal encryption — PgBouncer and client TLS rollout