Neon "password authentication failed" (pooled vs direct) in CI
Neon rejected the login because the password in DATABASE_URL does not match the role, or the host is the wrong endpoint variant. Neon issues two connection strings per branch: a direct one and a -pooler one, and each has its own host.
What this error means
A migration or test step fails connecting to Neon with "FATAL: password authentication failed for user 'X'". The same string may work locally but not in CI, or vice versa, because the pooled and direct hosts differ.
psql: error: connection to server at "ep-cool-name-123456.us-east-2.aws.neon.tech"
failed: FATAL: password authentication failed for user "neondb_owner"Common causes
CI stored a connection string that does not match the branch role
The DATABASE_URL secret holds a password for a different branch or a rotated role. Neon branch endpoints each carry their own role password, so a copied string from another branch fails.
The pooled and direct hosts were mixed up
Neon gives a direct host and a -pooler host. Using the pooled password against the direct host (or an outdated pooled string) surfaces as an auth failure rather than a host error.
How to fix it
Read the exact connection string from the Neon API for this branch
- Fetch the branch connection URI at CI time instead of storing a stale one.
- Set it as DATABASE_URL for the migration and test steps.
- Use the
-poolerURI for app connections and the direct URI for migrations.
# get the pooled URI for the branch created for this PR
neonctl connection-string "pr-${{ github.event.number }}" \
--project-id "$NEON_PROJECT_ID" --pooled >> conn.txtStore only the project id and API key, derive URLs in CI
Keep NEON_API_KEY and NEON_PROJECT_ID as secrets and generate the connection string per run so the password always matches the current branch role.
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}How to prevent it
- Generate the Neon connection string at CI time rather than pinning a static secret.
- Keep the pooled URI for app pools and the direct URI for migrations.
- Rotate the branch role and refresh the derived URL together, never by hand.