feat(db): support transaction-pooling poolers (pgbouncer / RDS Proxy / Supabase) - #299
Merged
telivity-otaip merged 2 commits intoAug 11, 2026
Merged
Conversation
postgres.js prepares statements by default. Under a transaction-pooling pooler
(pgbouncer, RDS Proxy, Supabase) the statement is prepared on one backend
connection and executed on another, so the API fails on its second concurrent
query with no way to opt out from configuration.
Poolers also commonly terminate TLS with a private or self-signed certificate,
and sslmode in the connection URL is not honoured consistently across
postgres.js versions.
Adds two environment switches, both defaulting to current behaviour so direct
connections are unchanged:
DATABASE_POOLER_MODE=transaction -> prepare: false
DATABASE_SSL=no-verify -> ssl: { rejectUnauthorized: false }
Applied at all three connection sites (api module, seed, push-schema) since the
init containers reach the database through the same pooler. Documented in both
env examples.
Maintainer polish on top of the pooler-support change: one postgresOptionsFromEnv helper for api/seed/push-schema, plus a louder MITM warning for DATABASE_SSL=no-verify in the env examples. Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
telivity-otaip
approved these changes
Aug 11, 2026
telivity-otaip
left a comment
Collaborator
There was a problem hiding this comment.
This is a solid fix — thank you, Charles. postgres.js named prepares under transaction pooling and unreliable URL sslmode were exactly the failure mode.
Merging with full credit to you. Small maintainer polish on top of your PR (not changing the design):
- shared
postgresOptionsFromEnvso api / seed / push-schema can’t drift - louder
DATABASE_SSL=no-verifywarning in.env.exampleand.env.production.example
(tools/haip-connect-gpt left alone — different package / TOOL_LOG_DATABASE_URL, no clean shared dep.)
Appreciate you running HAIP through a real pooler and sending this back.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Running HAIP against Postgres through pgbouncer in transaction mode, the API dies on its second concurrent query. Two causes, both in how
postgres.jsis initialised.1. Named prepared statements. postgres.js prepares by default. Under transaction pooling the statement is prepared on one backend connection and executed on another, which fails. The driver supports
prepare: falsefor exactly this case, but there's no way to reach it from configuration today.2. TLS. Poolers commonly terminate TLS with a private or self-signed certificate, and
sslmodein the connection URL is not honoured consistently across postgres.js versions — so a URL parameter looks like it works and doesn't.The change
Two environment switches, both defaulting to current behaviour so nothing changes for direct connections:
DATABASE_POOLER_MODE=transaction→prepare: falseDATABASE_SSL=no-verify→ssl: { rejectUnauthorized: false }Applied at all three connection sites (
apps/apidatabase module,seed,push-schema), since the init containers reach the database through the same pooler. Documented in.env.exampleand.env.production.example.Direct connections keep prepared statements, which are a real win when you own the connection — the switch only opts out where the pooler makes them impossible.
Testing
Verified against pgbouncer in transaction mode with a self-signed certificate: without these,
push-schemaexits onPostgresError: SSL requiredand the API fails on concurrent queries; with them, schema push and normal operation both succeed.pnpm build && pnpm -r testgreen.Happy to split the TLS half into its own PR if you'd prefer them separate.