Supabase guide

Supabase connection string: every mode, port and setting

Written by Varun Krishnan · Founder, dbdiagramr

Published · 10 min read

Supabase gives one database four connection modes: direct, shared session, shared transaction, and dedicated. The host, port, and username change between them. This guide shows which string belongs where, how to disable prepared statements, and what each connection error actually means.

The short answer

  • Serverless or edge: shared transaction pooler, port 6543, prepared statements off.
  • Persistent app on IPv4: shared session pooler, port 5432.
  • IPv6 network or IPv4 add-on: direct connection, port 5432.
  • Paid, high-performance app: dedicated pooler, port 6543, transaction mode only.
  • Migrations, pg_dump, schema tools: direct or shared session. Never the transaction pooler.

The shared pooler is IPv4 on every plan. The direct and dedicated endpoints are IPv6 by default; IPv4 needs Supabase’s paid IPv4 add-on. That one difference explains most ENOTFOUND reports.

All four Supabase connection modes

ModeHost patternPortIPv4 / IPv6 by planUsername formatPrepared statementsBest for
Directdb.[ref].supabase.co5432IPv6 by default; IPv4 add-onpostgresYesMigrations, pg_dump, long-lived IPv6 apps
Shared sessionaws-[n]-[region].pooler.supabase.com5432IPv4 on every planpostgres.[ref]YesPersistent IPv4 apps, BI tools, database GUIs
Shared transactionaws-[n]-[region].pooler.supabase.com6543IPv4 on every planpostgres.[ref]NoServerless, edge, short-lived connections
Dedicateddb.[ref].supabase.co6543Paid plans; IPv6 by default, IPv4 add-onpostgresNoHigh-performance apps on paid plans

The dedicated pooler shares a hostname with the direct connection and uses port 6543. It supports transaction mode only. A session-mode connection on a paid plan still goes through the shared pooler.

[n] is a pooler cluster index, not part of the region. A region can have more than one. Copy the full host from Dashboard → Connect; do not assemble it from a blog post or from memory.

Which mode should you use?

Match the mode to the lifetime of the connection, not to the framework name.

  1. Vercel, Cloudflare, Lambda, or another short-lived runtime: shared transaction on 6543. Turn off prepared statements in the driver.
  2. Long-running server on an IPv4-only network: shared session on 5432. It keeps session state and supports prepared statements.
  3. Long-running server with IPv6: direct on 5432. It has the fewest moving parts.
  4. High connection volume on a paid plan: dedicated pooler on 6543. Budget for transaction-mode limits.
  5. Migration or dump: direct or shared session. Transaction pooling cannot hold the session your tool needs.

When in doubt, a shared session string is the safest diagnostic default because it works on IPv4 and accepts prepared statements. Move to transaction mode once you know the deployment is serverless.

Copy the string from Supabase

  1. Open the Supabase Dashboard for the project.
  2. Choose Connect.
  3. Select Direct, Session pooler, Transaction pooler, or Dedicated pooler.
  4. Copy the complete string.
  5. Replace only the password placeholder.
postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=require

Keep the host and username exactly as shown. The shared pooler username is postgres.[ref], not postgres. A custom database role follows the same role.[ref] shape on the shared pooler.

Test the string before wiring it into the app:

psql "postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=require" -c "select 1"

If that succeeds, the host, port, username, password, and TLS settings are all working. If it fails, the error usually names the layer that broke.

Usernames are the second-most-common break

ModeUsernameWhat goes wrong
DirectpostgresAdding .[ref] produces an authentication failure.
DedicatedpostgresSame as direct: plain role, no project suffix.
Shared sessionpostgres.[ref]Dropping .[ref] gives Tenant or user not found.
Shared transactionpostgres.[ref]Dropping .[ref] gives the same tenant error.

The shared pooler has to find the tenant before it can find the role. Without the project ref, postgres is ambiguous. Supabase returns Tenant or user not found or FATAL: (ENOTFOUND) tenant/user postgres.[ref] not found when the host and username do not belong together.

The fix is not to guess. Copy both values from the same Connect string. If you rotate credentials, use the same role and project ref; the pooler may keep old credentials until existing connections recycle.

Passwords and URL encoding

A connection string is a URL. A raw @ changes where the password ends. A raw # starts a fragment, and a raw / starts the database path. Encode reserved characters before placing a password in the URI:

raw:      p@ss word
encoded:  p%40ss%20word
  • @ becomes %40
  • # becomes %23
  • ? becomes %3F
  • / becomes %2F
  • a space becomes %20

A password with only letters and numbers can go in unchanged. When in doubt, encode the password component with encodeURIComponent() in JavaScript or urllib.parse.quote() in Python.

If the server says FATAL: password authentication failed, check the username first, then the password. A correctly encoded password for the wrong role still fails. If you reset the password in the Dashboard, update every environment variable that uses it and restart the app so pooled connections reconnect with the new credentials.

SSL: require encrypts, verify-full verifies

Supabase accepts unencrypted connections unless you enable SSL enforcement. Set sslmode=require at minimum. It encrypts traffic, but it does not verify that the server is really Supabase.

To verify the server identity, download the Supabase root certificate from Database settings and use verify-full with sslrootcert:

postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=verify-full&sslrootcert=prod-ca-2021.crt

The certificate filename is prod-ca-2021.crt. Keep it out of source control if your deployment policy treats certificates as configuration. For most applications, sslmode=require is the practical default; verify-full is the stronger choice when you can ship and rotate the CA.

Do not use sslmode=prefer for Supabase. It allows a silent fallback to an unencrypted connection. Do not use sslmode=disable outside a local test.

Driver settings for transaction mode

Transaction pooling hands a server connection back after each transaction. Session state and named prepared statements do not survive. Each driver disables them differently:

DriverSettingNotes
Prisma?pgbouncer=trueUse a direct or session URL for migrations.
Drizzle + postgres.jsprepare: falseSet it when creating the client.
node-postgresOmit nameAn unnamed query is not a named prepared statement.
psycopg 3prepare_threshold=NoneDisables automatic prepared statements.

Prisma

DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:6543/postgres?sslmode=require&pgbouncer=true"
DIRECT_URL="postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=require"

On Prisma 6, put directUrl = env("DIRECT_URL") in the datasource block and use DATABASE_URL for the pooled runtime connection. On Prisma 7 and later, directUrl is deprecated: put the direct or session URL in prisma.config.ts as datasource.url, and create Prisma Client with a driver adapter such as @prisma/adapter-pg.

Prisma 7 recommends leaving out pgbouncer=true on PgBouncer 1.21 or newer. Supabase’s pooler is Supavisor, and Supabase still documents the flag for pooled Prisma connections. Keep it on the shared transaction string; it is the setting Prisma expects for a pooler that does not support prepared statements.

Drizzle with postgres.js

import postgres from "postgres";

const sql = postgres(process.env.DATABASE_URL!, { prepare: false });

node-postgres

await client.query({ text: "select 1" });

Leave out name. Supplying a name turns the query into a prepared statement that the next transaction may not find.

psycopg 3

import psycopg

conn = psycopg.connect(conninfo, prepare_threshold=None)

Supabase connection errors and what they mean

ErrorCauseFix
getaddrinfo ENOTFOUND db.[ref].supabase.coIPv4-only network hitting the IPv6-only direct host.Use shared session on 5432, or buy the IPv4 add-on.
FATAL: (ENOTFOUND) tenant/user postgres.[ref] not foundPooler host and username are from different modes or projects.Copy host and username from the same Connect string.
prepared statement "s0" already existsNamed prepared statements through transaction pooling.Disable them in the driver, or use direct or shared session.
FATAL: password authentication failedWrong role or password, or the password is not URL-encoded.Check the username, encode reserved characters, then reset if needed.
ENETUNREACH / ETIMEDOUTAn IPv6 address was returned but the network has no route or is blocked.Use the shared pooler or add IPv4.
Max client connections reached / No more connections allowed (max_client_conn)The pooler has exhausted its client connection limit.Use transaction mode, reduce pool size, close clients, or upgrade compute.

Start with the exact error text. ENOTFOUND is a DNS problem, ETIMEDOUT is a path problem, and password authentication failed is a credential problem. They point at different fixes.

What was tested

On October 23, 2026, both shared pooler modes connected to a 20-table test project and read the schema plus Supabase’s auth and storage tables. Session mode on 5432 and transaction mode on 6543 each completed in 2–4 seconds. Prepared statements were disabled for the transaction-mode run.

The direct endpoint was not tested from an IPv4-only network. Its IPv6 default and the IPv4 add-on behavior come from Supabase’s current connection docs, not from that test run.

Frequently asked questions

Sources

Keep going

Check your Supabase string

Paste a connection string into the free checker. It runs in your browser, flags the mode, port, username, SSL, and Prisma settings, and never sends the password anywhere.

Open the checker →