CapyDB/ docs
Guides

Credential rotation

What actually rotates, when the old password stops working, and which parts of your stack update themselves.

What rotates

A plain rotation replaces exactly one thing: the database password for the project's role. A project's runtime role (app_user), where it has one, rotates separately and is not touched. The hostname, ports, database name, and role name all stay the same - only the password segment of your connection URLs changes. API keys, webhook secrets, and integration tokens have their own rotation surfaces and are untouched.

A rotation with a grace period (grace_hours in the request body, --grace-hours on the CLI, up to 720) issues a new username and password instead. The outgoing credential keeps authenticating until the window ends - Postgres itself enforces the deadline - so running deployments can drain onto the new URLs, and the old role is dropped afterwards. Both usernames act as the same database owner, so nothing about object ownership changes.

The new password is generated server-side (a 24-byte random secret), stored AES-GCM encrypted in the control plane, and applied to your database with an ALTER ROLE. It is never chosen by, or shown to, anyone before it lands in your connection details.

Triggering a rotation

Rotating takes the organization admin role in the dashboard, or an organization-wide API key (the CLI, Terraform, CI). A workspace member without the admin role cannot rotate: a rotation without a grace period drops every live connection on the old password.

From the dashboard (the project's Connections tab or Settings → Rotate Credentials) or the API:

curl -X POST https://capydb.dev/api/capydb/v1/projects/$PROJECT_ID/credentials/rotate \
  -H "Authorization: Bearer $CAPYDB_API_KEY"

From the CLI:

capydb credentials rotate --wait

It asks you to confirm (--confirm skips the prompt), and capydb connection-string hands you the fresh URLs once the rotation completes. See the CLI reference for its flags.

Rotation is asynchronous: the response is a job, the project moves to a rotating_credentials state, and flips back to ready when the job completes. Poll GET /v1/jobs/{id} or listen for the events below. The job retries up to 3 times on transient failure; if it fails terminally, the old password remains valid and the project returns to ready.

Invalidation timing

  • Until the job completes, the old password keeps working. Rotation is requested, not instant.
  • Once the job completes, new connection attempts with the old password fail - on both the direct and pooled endpoints simultaneously, since both authenticate against the same database role. With a grace period, the old username and password keep working until the window you chose ends, then fail the same way.
  • Already-open connections are not terminated. A session established before the rotation keeps working until it disconnects on its own. If "no session may outlive the old credential" is a hard requirement (a leaked URL, an offboarding), rotate and then restart your app processes so every pool reconnects with the new password.

What updates itself

  • Connected Vercel and Netlify integrations re-push DATABASE_URL / DATABASE_URL_UNPOOLED automatically after the rotation job succeeds (the sync itself retries up to 5 times). Note that already-running deployments baked the old env vars at build/boot time - trigger a redeploy to pick up the new URLs.
  • A credentials.rotated webhook event fires (plus the generic job.completed), so your own automation can react.
  • The audit trail records project.credentials_rotated with the job ID and role name.

Everything else - CI secrets, local .env files, password managers, that one cron job on a forgotten VM - holds the old URL until you update it.

Runbook

  1. Pause anything that auto-restarts database workers mid-deploy, if reconnect storms matter to you.
  2. Trigger the rotation (dashboard or POST /v1/projects/{id}/credentials/rotate).
  3. Wait for completion: poll the job, or act on the credentials.rotated webhook.
  4. Fetch the new URLs: dashboard Connections tab, GET /v1/projects/{id}/connections, or capydb connection-string.
  5. Update every consumer that is not a connected Vercel/Netlify integration: CI secrets, other deploy targets, local env files.
  6. Redeploy / restart app processes - both to pick up pushed env vars and to drop any sessions still authenticated with the old password.
  7. Confirm: new connections succeed, and the audit trail shows the rotation.

When to rotate

  • A connection URL leaked - committed to a repo, pasted in a chat, logged somewhere.
  • Someone with database access left the team.
  • Routinely, if your security policy wants credential turnover; rotation is cheap and does not touch data.