From bc102876bbfb428582b57cacb7099d2ab8d764e9 Mon Sep 17 00:00:00 2001 From: Miranda Limonczenko Date: Wed, 9 Sep 2026 16:49:42 -0700 Subject: [PATCH] docs: apply the rest of the connecting to Postgres feedback (#49928) Closes FDBKIN-31335 Closes FDBKIN-13040 Closes FDBKIN-8653 Closes FDBKIN-19912 Closes DOCS-740 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update. While we are revising this document, this PR gathers docs feedback via AI magic and applies that feedback. ## What is the current behavior? These findings stand on feedback intake rather than on the baseline. Worth doing, and the eval won't show a score change for any of them. - **Nothing explains the pooler host.** #49868 switched the strings to `[POOLER-HOST]`, but the page never says why you can't compose the host, and agents that recite `aws-0` get `Tenant or user not found`. agent-skills#92. - **The page gives the instruction to turn prepared statements off, but not the flag.** It also links the GitHub discussion rather than the troubleshooting entry that mirrors it. FDBKIN-8248, FDBKIN-7883. - **SSL goes undiscussed.** Four of six eval runs set `ssl: 'require'` unprompted. - **The pooled username format only appears inside example strings**, never as a rule. DOCS-740, FDBKIN-19912. - **Third-party tools have no answer.** Session mode is the right one, and the decision table had no row for a BI client or database GUI at all. FDBKIN-8653. - **Only one of transaction mode's three limitations is documented.** FDBKIN-13040 names prepared statements, cursors, and session-level settings. The page covered prepared statements. ## What is the new behavior? - Tell the reader to copy the host, port, and username rather than typing the placeholders, and explain the pooler cluster index next to the reference table. The placeholders themselves changed in #49868. - State the username rule: direct connections and the dedicated pooler use `postgres`, shared pooler connections use `postgres.`. - Add a per-driver prepared statements table for Postgres.js, Drizzle, Prisma, asyncpg, and JDBC, and link [Disabling prepared statements](https://supabase.com/docs/guides/troubleshooting/disabling-prepared-statements-qL8lEL) for the rest. Add JDBC's `prepareThreshold=0` to that entry too, so the two pages agree. - Document SSL: `require` rather than the `prefer` default, which falls back to plaintext. - Link the `CONNECT_TIMEOUT` entry for stale sockets in frozen serverless runtimes. - Add a decision table row for a third-party tool, and point at Quickstarts for named tools. - Cover all three transaction mode limitations. Cursors work inside a single transaction only, and session-level state is lost between transactions: `set` and `reset`, session-level advisory locks, `listen` and `notify`, and temporary tables. Renamed the section from "Prepared statements", since it now covers the cause rather than one symptom. - Promote Configure your client to an H2 and fold the SSL certificate section into it. The table of contents only renders H2 and H3, so the client settings were invisible as H4s. ## Manual testing 1. Open [Connect to your database](https://docs-git-docs-connecting-to-postgres-technical-supabase.vercel.app/docs/guides/database/connecting-to-postgres) on the deploy preview. 2. Read the Get your connection string lead-in. It tells you to copy the host, port, and username rather than typing the placeholders. 3. Check the table of contents. Configure your client is an H2 with Application-side pool size, Prepared statements, SSL, and Stale connections under it. 4. Follow the prepared statements link. It lands on the in-docs troubleshooting entry, not GitHub. 5. Open the [endpoint reference](https://docs-git-docs-connecting-to-postgres-technical-supabase.vercel.app/docs/guides/database/connecting-to-postgres#endpoints-and-ip-versions). The table shows `aws-[INDEX]-[REGION]`, and the prose below explains the index and the username rule. 6. Read the decision table. It has a row for a third-party BI client or database GUI, pointing at session mode. 7. Read [Transaction mode limitations](https://docs-git-docs-connecting-to-postgres-technical-supabase.vercel.app/docs/guides/database/connecting-to-postgres#transaction-mode-limitations). It covers prepared statements, cursors, and session-level state. ## Summary by CodeRabbit ## Documentation - Expanded the Postgres connection guide with clearer client configuration guidance, including pool sizing, SSL, stale connections, and transaction mode limitations. - Added recommendations for BI tools and database GUIs using the shared pooler. - Clarified connection strings, pooler hosts, usernames, ports, and IP version behavior. - Updated serverless driver guidance for transaction mode configuration. - Added JDBC troubleshooting instructions for disabling prepared statements with `prepareThreshold=0`. --- .../database/connecting-to-postgres.mdx | 111 ++++++++++++------ .../serverless-drivers.mdx | 2 +- .../disabling-prepared-statements-qL8lEL.mdx | 8 ++ 3 files changed, 84 insertions(+), 37 deletions(-) diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index 61e94e3ad14..df6115b4621 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -8,6 +8,7 @@ Learn how to pick a connection method and where to find the connection string fo - [Which connection method do you use?](#choose-a-connection-method) picks a method based on where your code runs. - [Get your connection string](#get-your-connection-string) shows where each string comes from. +- [Configure your client](#configure-your-client) sets pool size, prepared statements, and SSL. - [Quickstarts](#quickstarts) connect a specific ORM or database GUI. For how pooling works and the limits that apply to your connections, see [Connection pooling and limits](/docs/guides/database/connecting-to-postgres/pooling-and-limits). @@ -22,6 +23,7 @@ How you connect to your database depends on where your code runs. Find your case | A serverless or edge function | [Shared pooler, transaction mode](#pooler-transaction-mode) | These environments open many short-lived connections. | | A persistent backend on IPv6, or with the IPv4 add-on | [Direct connection](#direct-connection) | No pooler in the path. | | A persistent backend on an IPv4-only network | [Shared pooler, session mode](#pooler-session-mode) | The shared pooler is IPv4-only on every plan. | +| A third-party tool, such as a BI client or database GUI | [Shared pooler, session mode](#pooler-session-mode) | Reachable over IPv4 from networks you don't control, and it supports prepared statements. | | A high-performance application on a paid plan | [Dedicated pooler](#dedicated-pooler) | Runs on the same machine as your database, so lower latency than the shared pooler. | | Migrations, `pg_dump`, backup and restore, or replication | [Direct connection](#direct-connection) | These are single sessions and Postgres native commands. | @@ -31,7 +33,7 @@ The IPv4 add-on is not dual-stack: enabling it swaps the project's IPv6 (AAAA) D -For the host, port, and IP version of each mode, see [Endpoints and IP versions](#endpoints-and-ip-versions). +For the host, port, and IP version of each mode, see [Endpoints and IP versions](#endpoints-and-ip-versions). For a named ORM or database GUI, see [Quickstarts](#quickstarts). ## Get your connection string [#get-your-connection-string] @@ -44,7 +46,7 @@ For every Postgres connection mode, the string comes from the same place: 3. Choose the connection method you picked above. 4. Copy the string and replace `[YOUR-PASSWORD]` with your database password. [Percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) any reserved characters it contains, such as `&`, `#`, `?`, or a space. -The sections below show what each string looks like and when to use it. +The sections below show what each string looks like and when to use it. Take the host, port, and username from the string you copied rather than typing the bracketed placeholders literally. The pooler host in particular can't be composed from your region, and pooled connections use a different username from direct connections. Both are covered under [Endpoints and IP versions](#endpoints-and-ip-versions). ### Direct connection [#direct-connection] @@ -78,7 +80,7 @@ The transaction mode connection string connects to your Postgres instance throug -Transaction mode does not support [prepared statements](https://postgresql.org/docs/current/sql-prepare.html). To avoid errors, [turn off prepared statements](https://github.com/orgs/supabase/discussions/28239) for your connection library. +Transaction mode does not support [prepared statements](https://postgresql.org/docs/current/sql-prepare.html). To avoid errors, turn them off in your connection library. See [Transaction mode limitations](#transaction-mode-limitations) for the setting your driver uses and for the other session-state features this affects. @@ -98,31 +100,6 @@ postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres Get this string from the Supabase Dashboard by clicking [Connect](/dashboard/project/_?showConnect=true&method=transaction). -### Configure your client - -Your connection library keeps its own pool of connections, separate from the poolers Supabase runs. Configure it for the environment your code runs in. - -In a serverless function: - -1. Create the client once at module scope, not per request. -2. Set the pool to 1 connection. The client is shared by every invocation on that warm instance, so this caps the instance, not the request. -3. Turn off prepared statements, which [transaction mode](#pooler-transaction-mode) doesn't support. - -```ts lib/db.ts -import postgres from 'postgres' - -export const sql = postgres(process.env.DATABASE_URL, { - max: 1, - prepare: false, -}) -``` - -Library defaults assume a persistent backend, so they are too high for serverless. [Postgres.js](https://github.com/porsager/postgres) defaults to 10 connections. That is 10 connections for every warm instance of your function, and the number of warm instances isn't something you control. A few dozen instances is enough to exhaust the pool. - -Raise the pool above 1 only when you have evidence that concurrent invocations on one instance are queuing for the connection. - -For more on sizing an application-side pool, see the [Supavisor FAQ](/docs/guides/troubleshooting/supavisor-faq-YyP5tI). If you use Prisma, [Prisma troubleshooting](/docs/guides/database/prisma/prisma-troubleshooting) covers the equivalent `connection_limit` setting. - ### Data APIs and client libraries [#data-apis-and-client-libraries] The Data APIs let you interact with your database using REST or GraphQL requests. You can use these APIs to fetch and insert data from the frontend, as long as your tables have [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS) enabled and policies that allow the access. RLS with no policies denies every request. @@ -139,14 +116,6 @@ For convenience, you can also use the [Supabase client libraries](/docs/referenc - [C#](/docs/reference/csharp/introduction) - [Kotlin](/docs/reference/kotlin/introduction) -### Connect with SSL [#connecting-with-ssl] - -Connect to your database using SSL wherever possible, to prevent snooping and man-in-the-middle attacks. - -Download your server root certificate from [Database settings](/dashboard/project/_/database/settings) in the Supabase Dashboard. The same section has a toggle that rejects non-SSL connections to your database. - -![The SSL Configuration section of Database settings, with a toggle to enforce SSL on incoming connections and a Download Certificate button.](/docs/img/database/database-settings-ssl.png) - ### Endpoints and IP versions [#endpoints-and-ip-versions] Each mode has its own host, port, and IP version support. IP version support depends on your plan and on whether the project has the [IPv4 add-on](/docs/guides/platform/ipv4-address). @@ -158,10 +127,80 @@ Each mode has its own host, port, and IP version support. IP version support dep | Shared pooler, transaction mode | `aws-[INDEX]-[REGION].pooler.supabase.com:6543` | IPv4 | IPv4 | IPv4 | | Dedicated pooler, transaction mode | `db.[PROJECT-REF].supabase.co:6543` | - | IPv6 | IPv4 | +`[INDEX]` in the shared pooler host is a pooler cluster index, not part of the region name. A region can have more than one, so you can't work out your host from your region. Copy the host from the Connect dialog. + +The username differs by connection type. Direct connections and the dedicated pooler use `postgres`. Shared pooler connections use `postgres.[PROJECT-REF]`. If you connect as a custom role through the shared pooler, the username is `[ROLE].[PROJECT-REF]`. + The port routes the connection to the right pooler and mode. Port `5432` reaches Postgres for a direct connection and Supavisor for session mode. Port `6543` reaches PgBouncer for the dedicated pooler and Supavisor for shared transaction mode. To connect over IPv4, you have two options. The shared pooler is IPv4-only on every plan, in both session and transaction mode. Alternatively, add the [IPv4 add-on](/docs/guides/platform/ipv4-address) to your project, which makes the direct connection and the dedicated pooler reachable over IPv4 instead of IPv6. +## Configure your client + +A connection string on its own isn't enough. Your connection library keeps its own pool of connections, separate from the poolers Supabase runs, and its defaults assume a persistent backend. + +In a serverless function: + +1. Create the client once at module scope, not per request. +2. Set the pool to 1 connection. The client is shared by every invocation on that warm instance, so this caps the instance, not the request. +3. Turn off prepared statements, which [transaction mode](#pooler-transaction-mode) doesn't support. +4. Set SSL to `require`, so the driver refuses to connect without encryption. + +```ts lib/db.ts +import postgres from 'postgres' + +export const sql = postgres(process.env.DATABASE_URL, { + max: 1, + prepare: false, + ssl: 'require', +}) +``` + +The rest of this section explains each setting, and what changes for a driver other than Postgres.js. + +### Application-side pool size + +Library defaults are too high for serverless. [Postgres.js](https://github.com/porsager/postgres) defaults to 10 connections. That is 10 connections for every warm instance of your function, and the number of warm instances isn't something you control. A few dozen instances is enough to exhaust the pool. + +Raise the pool above 1 only when you have evidence that concurrent invocations on one instance are queuing for the connection. + +For more on sizing an application-side pool, see the [Supavisor FAQ](/docs/guides/troubleshooting/supavisor-faq-YyP5tI). If you use Prisma, [Prisma troubleshooting](/docs/guides/database/prisma/prisma-troubleshooting) covers the equivalent `connection_limit` setting. + +### Transaction mode limitations + +[Transaction mode](#pooler-transaction-mode) returns your connection to the pool after each transaction, so anything that depends on session state doesn't survive between transactions. Three things are affected. + +**Prepared statements** aren't supported, so turn them off. Each driver does this differently: + +| Driver | Setting | +| -------------------- | ----------------------------------------- | +| Postgres.js, Drizzle | `prepare: false` | +| Prisma | `pgbouncer=true` on the connection string | +| asyncpg | `statement_cache_size=0` | +| JDBC | `prepareThreshold=0` | + +For node-postgres, Psycopg, and Rust drivers, see [Disabling prepared statements](/docs/guides/troubleshooting/disabling-prepared-statements-qL8lEL). + +**Cursors** work inside a single transaction only. A `with hold` cursor is meant to outlive its transaction, and it doesn't survive the connection returning to the pool. + +**Session-level state** is lost between transactions. This covers `set` and `reset`, session-level advisory locks, `listen` and `notify`, and temporary tables. Run them inside the transaction that needs them, or use session mode or a direct connection instead. + +Direct connections and session mode support all three, so none of this applies to either. + +### SSL [#connecting-with-ssl] + +Connect using SSL wherever possible, to prevent snooping and man-in-the-middle attacks. + +Set SSL to `require` so the driver refuses to connect without encryption. Most drivers default to `prefer`, which falls back to sending your data in plaintext if the encrypted attempt fails. On a connection string, this is `sslmode=require`. + +`require` encrypts the connection but doesn't verify the server, so it doesn't stop a man-in-the-middle attack. To verify as well as encrypt, download your server root certificate from [Database settings](/dashboard/project/_/database/settings) in the Supabase Dashboard and point your driver at it. Downloading the certificate on its own changes nothing: the driver has to be told to use it, with `sslmode=verify-full` and `sslrootcert` on a connection string, or the equivalent option in your library. The same section has a toggle that rejects non-SSL connections to your database. + +![The SSL Configuration section of Database settings, with a toggle to enforce SSL on incoming connections and a Download Certificate button.](/docs/img/database/database-settings-ssl.png) + +### Stale connections + +Serverless runtimes freeze a function between requests, which can leave a pooled TCP socket stale. If you see `CONNECT_TIMEOUT` errors or queries that hang until the execution limit, see [Troubleshooting `CONNECT_TIMEOUT` or hanging queries in Serverless Functions](/docs/guides/troubleshooting/troubleshooting-connect_timeout-or-hanging-queries-in-vercel-serverless-functions-775f92). + ## Quickstarts [#quickstarts] Each quickstart connects one ORM or database GUI to your Supabase database. diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx index 36642ee20a3..7ddc03b5775 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx @@ -9,7 +9,7 @@ Learn how to connect to your Postgres database from a serverless environment. Th [supabase-js](/docs/reference/javascript/introduction) is an isomorphic JavaScript client that uses the [auto-generated REST API](/docs/guides/api), so it works in any environment that supports HTTPS connections. This API has a built-in [connection pooler](/docs/guides/database/connecting-to-postgres/pooling-and-limits#how-connection-pooling-works) and can serve thousands of simultaneous requests, which suits serverless workloads. -If you connect with a Postgres client instead, use the [shared pooler in transaction mode](/docs/guides/database/connecting-to-postgres#pooler-transaction-mode). Transaction mode doesn't support [prepared statements](https://postgresql.org/docs/current/sql-prepare.html), so [turn them off](https://github.com/orgs/supabase/discussions/28239) in your connection library. +If you connect with a Postgres client instead, use the [shared pooler in transaction mode](/docs/guides/database/connecting-to-postgres#pooler-transaction-mode), then [configure your client](/docs/guides/database/connecting-to-postgres#configure-your-client) for pool size, prepared statements, and SSL. Those three settings are what most serverless connection problems come down to. ## Vercel Edge Functions diff --git a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx index 7d0c3ab413d..f563224808d 100644 --- a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx +++ b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx @@ -51,6 +51,14 @@ Follow the recommendation in the [asyncpg docs](https://magicstack.github.io/asy > disable automatic use of prepared statements by passing `statement_cache_size=0` to [asyncpg.connect()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.connect) and [asyncpg.create_pool()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool) (and, obviously, avoid the use of [Connection.prepare()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.Connection.prepare)); +## JDBC + +Set [`prepareThreshold`](https://jdbc.postgresql.org/documentation/use/#connection-parameters) to `0`: + +``` +jdbc:postgresql://[POOLER-HOST]:6543/postgres?prepareThreshold=0 +``` + ## Rust's Deadpool or `tokio-postgres`: - Check [GitHub Discussion](https://github.com/bikeshedder/deadpool/issues/340#event-13642472475)