fix(auth): set search_path per connection, not only via startup options - #185
fix(auth): set search_path per connection, not only via startup options#185wmadden-electric wants to merge 1 commit into
Conversation
The auth module's pool set search_path via the libpq `options` startup param. Prisma Postgres honors it, but the `prisma-composer dev` local Postgres emulator (and poolers such as pgbouncer) drop the startup `options` param, so Better Auth's schema-unqualified queries resolved against `public` and failed with `relation "user" does not exist`. Also issue `SET search_path` on each new pool connection, which is honored anywhere an ordinary query runs — the emulator, pgbouncer, and Prisma Postgres alike. Verified end-to-end: a fresh `prisma-composer dev` stack now completes email+password signup with no manual DB changes. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Summary by CodeRabbit
WalkthroughUpdated 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts`:
- Around line 68-72: Update the pool connection setup around the current
pool.on('connect') handler to use the Pool onConnect hook instead. Await the SET
search_path query and propagate failures by throwing rather than only logging
them, so node-postgres discards the misconfigured client and authentication
never uses an untrusted schema.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f57af175-631e-4aa5-8d86-c0dc76581943
📒 Files selected for processing (1)
packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts
| pool.on('connect', (client) => { | ||
| void client | ||
| .query(`SET search_path TO ${AUTH_SCHEMA}`) | ||
| .catch((err) => console.error('auth: failed to set search_path', err)); | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file outline =="
ast-grep outline packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts --view expanded || true
echo "== relevant file content =="
cat -n packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts | sed -n '1,140p'
echo "== package files =="
fd -a 'package.json' packages/1-prisma-cloud/2-shared-modules/auth packages/1-prisma-cloud | sort | while read -r f; do
echo "--- $f"
sed -n '/"dependencies"/,/^ },$/p' "$f" | sed -n '1,120p'
done
echo "== search pg pool usage =="
rg -n "new pg\.Pool|pool\.on\('connect'|onConnect|search_path|Better Auth|BetterAuth" packages/1-prisma-cloud/2-shared-modules -S || trueRepository: prisma/composer
Length of output: 21969
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lock pg entries =="
for f in pnpm-lock.yaml package-lock.json yarn.lock bun.lockb bun.lock; do
if [ -f "$f" ]; then
echo "--- $f"
rg -n "pg@|/pg@|name: pg|version: .*pg" "$f" | head -80 || true
fi
done
echo "== local pg types source if present =="
if [ -d node_modules/pg ]; then
fd . node_modules/pg -t f | head -50
rg -n "onConnect|addEventListener\\('connect'|\.emit\\('connect'|removePoolClient|removeClient|return\\(.*Client\\)|function Pool" node_modules/pg node_modules/@types/pg 2>/dev/null | head -160 || true
else
echo "node_modules/pg not present"
fi
echo "== package manager files =="
fd . -d 3 | rg '(^|/)pnpm-workspace.yaml$|^|package.json$' | sed -n '1,80p'Repository: prisma/composer
Length of output: 4238
🌐 Web query:
node-postgres pool onConnect hook async client query SET search_path rejects connection
💡 Result:
In node-postgres, the onConnect configuration option is the official and recommended way to perform setup tasks, such as executing SET search_path, on a new client [1][2]. If the asynchronous function provided to onConnect throws an error or returns a rejected promise, node-postgres will destroy the client and propagate the error to the caller who requested the connection (e.g., via pool.connect() or pool.query()) [1][2]. This behavior ensures that the pool does not hand out incorrectly configured clients [1]. Example usage: const pool = new Pool({ onConnect: async (client) => { await client.query('SET search_path TO my_schema'); }, }); Historically, users attempted to use the pool's 'connect' event for this purpose, but that event is not designed to wait for asynchronous operations, which often led to race conditions where queries were executed before the setup was complete [2][3][4]. Using onConnect avoids these issues by ensuring setup is performed before the client is made available to the pool [1][2]. Alternatively, for simple configuration tasks like setting the search path, you can also use the connection options directly in the pool configuration, which is set once at connection time and does not require executing a separate query for every new client: const pool = new Pool({ options: '-c search_path=my_schema', }); This approach is often more efficient than running a setup query via onConnect [5][6].
Citations:
- 1: Add
onConnectcallback which allows "setup" of a pooled client brianc/node-postgres#3620 - 2: https://node-postgres.com/apis/pool
- 3: https://github.com/brianc/node-postgres-docs/blob/master/content/api/1-pool.mdx
- 4: Future Support for Setup Queries When a New Pool Client Connects brianc/node-postgres#3617
- 5: How do you make set search path work when using connection pool? brianc/node-postgres#2619
- 6: need connection support currentSchema brianc/node-postgres#1123
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== pg pool source =="
fd -a 'pool\.js$|pool\.d\.ts$' node_modules/pg node_modules/pg-pool node_modules/@types/pg 2>/dev/null | while read -r f; do
echo "--- $f"
wc -l "$f" 2>/dev/null || true
done
echo "== pg-pool location =="
fd -a . node_modules | rg '(^|/)node_modules/pg-pool/package.json$|(^|/)node_modules/pg-pool/lib/' | head -100
echo "== pg-pool lib source snippets =="
if [ -d node_modules/pg-pool/lib ]; then
stat -c '%n: %s bytes' node_modules/pg-pool/lib/index.js || true
rg -n "onConnect|addEventListener\\('connect'|remove.*Client|return.*Client|reject\\(|Promise\\.allSettled|emit\\('connect'" node_modules/pg-pool/lib/index.js node_modules/pg-pool 2>/dev/null | head -200 || true
echo "== pool source around connect/add/remove =="
grep -n "onConnect\\|addEventListener\\|remove.*Client\\|return.*Client\\|connect\\|acquire\\|remove" node_modules/pg-pool/lib/index.js | sed -n '1,220p'
fiRepository: prisma/composer
Length of output: 284
🌐 Web query:
node-postgres 8.22.0 Pool onConnect TypeScript onConnect?: number return removed client destroyed
💡 Result:
The onConnect callback was introduced in node-postgres version 8.20.0 [1][2] to allow for the asynchronous initialization of newly created and connected pooled clients [1]. When you define onConnect in the Pool configuration, it is executed exactly once for each new client immediately after the client has successfully established a connection to the PostgreSQL backend, but before it is made available for use in the pool [3]. Regarding the behavior if the function fails: If the onConnect function throws an error or returns a promise that rejects, the library automatically destroys that specific client instance [3][4]. The associated error is then returned to the original caller (e.g., the code that called pool.connect() or pool.query()) that triggered the pool to create that new client [4]. Regarding TypeScript usage: The onConnect callback is typed to accept a client instance and may return either void or a Promise<void> [3]. Example implementation: const pool = new Pool({ onConnect: async (client) => { await client.query('SET search_path TO my_schema'); }, }); This mechanism is the recommended approach for running setup commands (such as setting the search path, time zones, or other session-specific configurations) on pooled clients, replacing the older and less reliable practice of using the pool.on('connect', ...) event listener for initialization [3][4][5].
Citations:
- 1: https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md
- 2: https://npmx.dev/package-changelog/pg/v/8.22.0
- 3: https://node-postgres.com/apis/pool
- 4: Add
onConnectcallback which allows "setup" of a pooled client brianc/node-postgres#3620 - 5: Future Support for Setup Queries When a New Pool Client Connects brianc/node-postgres#3617
Fail closed when setting search_path.
pool.on('connect') is not awaited and the current handler treats SET failures as only logged errors, so Better Auth can use a client configured with the wrong schema. Use the Pool onConnect hook, await the setup query, and throw so node-postgres removes the client and fails the connection instead of exposing an untrusted search path.
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 68-69: Avoid SQL injection
Context: client
.query(SET search_path TO ${AUTH_SCHEMA})
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
(sql-injection-typescript)
🪛 OpenGrep (1.25.0)
[ERROR] 69-70: SQL query built via string concatenation or template literal passed to query()/execute(). Use parameterized queries instead.
(coderabbit.sql-injection.raw-query-concat-js)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts` around
lines 68 - 72, Update the pool connection setup around the current
pool.on('connect') handler to use the Pool onConnect hook instead. Await the SET
search_path query and propagate failures by throwing rather than only logging
them, so node-postgres discards the misconfigured client and authentication
never uses an untrusted schema.
Source: MCP tools
Problem
The auth module's Postgres pool sets
search_pathvia the libpqoptions=-c search_path=<schema>startup parameter. Prisma Postgres honors this, but theprisma-composer devlocal Postgres emulator (and connection poolers such as pgbouncer) drop the startupoptionsparam. Better Auth's schema-unqualified queries then resolve againstpublicand fail:So email+password signup 500s under
prisma-composer dev, even though the auth pack correctly creates the tables in theauthschema.Fix
Also issue
SET search_path TO <AUTH_SCHEMA>on every new pool connection (pool.on('connect', …)). An in-sessionSETis honored anywhere an ordinary query runs — the dev emulator, pgbouncer, and Prisma Postgres alike. The startupoptionsparam is kept as the fast path where it works, so there is no behavior change on Prisma Postgres.Verification
Reproduced against a real consumer (a Splitwise app pinned to
0.3.0-dev.9): with a freshprisma-composer dev --freshstack and no manual database changes, email+password signup went from500 (relation "user" does not exist)to200.Notes
ALTER ROLE/ALTER DATABASE … SET search_path) do not work around this on the emulator — it storespg_db_role_settingbut new backends still come upsearch_path=public. Only an in-sessionSETis reliable there.node_modules, so lint/typecheck/tests run in CI. The change is a 3-linepool.on('connect', …)handler.🤖 Generated with Claude Code