Skip to content

fix(auth): set search_path per connection, not only via startup options - #185

Open
wmadden-electric wants to merge 1 commit into
mainfrom
claude/auth-search-path-on-connect
Open

fix(auth): set search_path per connection, not only via startup options#185
wmadden-electric wants to merge 1 commit into
mainfrom
claude/auth-search-path-on-connect

Conversation

@wmadden-electric

Copy link
Copy Markdown
Contributor

Problem

The auth module's Postgres pool sets search_path via the libpq options=-c search_path=<schema> startup parameter. Prisma Postgres honors this, but the prisma-composer dev local Postgres emulator (and connection poolers such as pgbouncer) drop the startup options param. Better Auth's schema-unqualified queries then resolve against public and fail:

[Better Auth]: relation "user" does not exist

So email+password signup 500s under prisma-composer dev, even though the auth pack correctly creates the tables in the auth schema.

Fix

Also issue SET search_path TO <AUTH_SCHEMA> on every new pool connection (pool.on('connect', …)). An in-session SET is honored anywhere an ordinary query runs — the dev emulator, pgbouncer, and Prisma Postgres alike. The startup options param 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 fresh prisma-composer dev --fresh stack and no manual database changes, email+password signup went from 500 (relation "user" does not exist) to 200.

Notes

  • Catalog-level defaults (ALTER ROLE / ALTER DATABASE … SET search_path) do not work around this on the emulator — it stores pg_db_role_setting but new backends still come up search_path=public. Only an in-session SET is reliable there.
  • Not run locally: the change was authored in a worktree without node_modules, so lint/typecheck/tests run in CI. The change is a 3-line pool.on('connect', …) handler.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Improved database connection reliability by consistently applying the configured schema search path.
    • Added graceful handling and logging when applying connection settings fails.

Walkthrough

Updated hardenedPool() to set the auth schema search path for every newly established database connection through a connect listener. The listener executes SET search_path TO ${AUTH_SCHEMA} and logs errors, while the existing pool startup option remains in place.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes setting search_path on every connection, which matches the main change.
Description check ✅ Passed The description is directly about the auth pool search_path fix and matches the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/auth-search-path-on-connect
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/auth-search-path-on-connect

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@185
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@185

commit: 817ca6e

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ba0314 and 817ca6e.

📒 Files selected for processing (1)
  • packages/1-prisma-cloud/2-shared-modules/auth/src/auth-options.ts

Comment on lines +68 to +72
pool.on('connect', (client) => {
void client
.query(`SET search_path TO ${AUTH_SCHEMA}`)
.catch((err) => console.error('auth: failed to set search_path', err));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 || true

Repository: 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:


🏁 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'
fi

Repository: 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:


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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant