Skip to content

Supabase Community

Status: tested against querycop HEAD on 2026-05-18. Endpoint conventions cited as of 2026-05.

Supabase hosts PostgreSQL on AWS and fronts it with the Supavisor connection pooler. Each project exposes two distinct connection endpoints:

  • Direct connection: db.<project-ref>.supabase.co:5432 (one connection per client, full PG semantics, session-bound state works)
  • Supavisor pooler: aws-0-<region>.pooler.supabase.com (transaction mode on port 6543, session mode on port 5432; connection density at the cost of session-bound features)

Supabase does not currently offer an IAM / OAuth equivalent for PostgreSQL PasswordMessage injection on the data plane — auth is password-based — so the IAM auth section is intentionally omitted from this page. (Supabase Auth issues JWTs for the REST / Realtime APIs, which are a different surface and not relevant to the PostgreSQL wire path.)

For the direct endpoint with password auth, the production-ready config is:

Env varValue
GATEKEEPER_BACKEND_HOSTdb.abcdefghijkl.supabase.co
GATEKEEPER_BACKEND_PORT5432
GATEKEEPER_BACKEND_TLS_MODEverify-full
GATEKEEPER_BACKEND_TLS_CA_FILE/etc/ssl/certs/supabase-prod-ca.pem (downloaded from dashboard)
GATEKEEPER_BACKEND_TLS_SERVER_NAME(unset — derived from BACKEND_HOST)

For the Supavisor pooler endpoint, swap the host / port and adjust the client-side username — see the Pattern B section below.

⚠️ The direct endpoint requires IPv6 on the Querycop host unless your project has the IPv4 add-on enabled. Supabase’s db.<project-ref>.supabase.co resolves to an IPv6-only address on the default plan, so a Querycop deployed in a typical VM / container without working IPv6 routing will fail to dial the backend. If you don’t have IPv6, either enable the IPv4 add-on on the project, or use the Pattern B Supavisor pooler endpoint (which is reachable over IPv4). See Gotchas for the full diagnostic + workaround.

  • A Supabase project. From the dashboard, you can read both endpoint forms under Settings → Database → Connection string.
  • The downloaded per-project CA certificate. Supabase serves certs signed by their own intermediate CA, and the dashboard exposes the chain you need under Settings → Database → SSL Configuration → Download certificate. Save the PEM somewhere Querycop can read.
  • The PostgreSQL role’s password from the dashboard (Settings → Database → Database password). This is the postgres user’s password by default; you can also create application-specific roles in the SQL editor.
  • Network reachability from the Querycop host to the chosen endpoint over the public internet. No VPC peering on the standard tier.
  • For the direct endpoint: working IPv6 outbound from the Querycop host (the direct endpoint resolves IPv6-only by default), OR the IPv4 add-on enabled on the project. If neither is available, use the pooler endpoint (Pattern B), which is reachable over IPv4. See Gotchas for the full IPv6 discussion.
  • Querycop with backend TLS support (GATEKEEPER_BACKEND_TLS_*).

Pattern A — Direct endpoint (password auth)

Section titled “Pattern A — Direct endpoint (password auth)”

The direct endpoint is a 1:1 connection to the underlying Postgres instance. Use it when the client needs full PG semantics:

  • Server-side prepared statements
  • LISTEN / NOTIFY (for pg_notify-driven workflows)
  • Session GUCs (SET search_path, SET ROLE)
  • Temp tables that survive across multiple statements
Terminal window
# From the dashboard: Settings → Database → SSL Configuration
# → Download certificate. Save the PEM somewhere readable.
mv ~/Downloads/prod-ca-2021.crt /etc/ssl/certs/supabase-prod-ca.pem
# Sanity-check
grep -c BEGIN /etc/ssl/certs/supabase-prod-ca.pem # should be ≥ 1

Supabase has rotated their root CA twice in recent history (most recently from prod-ca-2021 to a Let’s Encrypt-issued chain on some projects). Re-download whenever the dashboard prompts. There is no in-process hot-reload of BACKEND_TLS_CA_FILE — restart Querycop after replacing the file.

Terminal window
# Required: the direct endpoint
export GATEKEEPER_BACKEND_HOST=db.abcdefghijkl.supabase.co
export GATEKEEPER_BACKEND_PORT=5432
# Required: full TLS verification against the per-project CA
export GATEKEEPER_BACKEND_TLS_MODE=verify-full
export GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/supabase-prod-ca.pem
# SERVER_NAME not set — Querycop derives it from BACKEND_HOST,
# which matches the SAN Supabase issues for the endpoint.
# Standard Querycop runtime
export GATEKEEPER_LISTEN_PORT=15432
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop
Terminal window
PGPASSWORD='<dashboard password>' psql \
-h 127.0.0.1 -p 15432 \
-U postgres -d postgres \
-c 'select 1'

verify-full is the right default for production. require (no certificate verification) is evaluation-only — useful when you’re standing up against a brand-new project and haven’t yet downloaded the CA from the dashboard, then switch back to verify-full. disable is not an appropriate choice — Supabase endpoints are reached over the public internet, and disable would put the password on the open wire.

The pooler endpoint serves a different operational tradeoff:

Pooler portModeUse for
6543TransactionHigh request rate, short queries, no session-bound state. Most HTTP app servers.
5432SessionLike a direct connection, but with the pooler’s load-balancing in front. Session-bound state works.

The pooler requires the client-side username to include the project ref:

direct endpoint: -U postgres
pooler endpoint: -U postgres.abcdefghijkl
└─ DB role └─ project ref

The project ref is the same string that appears in the direct endpoint hostname (db.<project-ref>.supabase.co). Without this format, the pooler can’t route the connection to the right project.

Terminal window
export GATEKEEPER_BACKEND_HOST=aws-0-us-east-1.pooler.supabase.com
# Transaction-mode pooling (most apps)
export GATEKEEPER_BACKEND_PORT=6543
# Observability-only flag — match the port choice above.
export GATEKEEPER_BACKEND_POOLER=pgbouncer-txn
# Or session-mode pooling — uncomment both lines and comment the 6543
# pair above
# export GATEKEEPER_BACKEND_PORT=5432
# export GATEKEEPER_BACKEND_POOLER=pgbouncer-session
export GATEKEEPER_BACKEND_TLS_MODE=verify-full
export GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/supabase-prod-ca.pem
# SERVER_NAME derived from BACKEND_HOST (pooler hostname).
export GATEKEEPER_LISTEN_PORT=15432
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop

GATEKEEPER_BACKEND_POOLER doesn’t change the wire behavior — Querycop forwards client SQL unchanged either way. It just labels the startup log so operators can confirm at a glance that this Querycop is wired to the pooler (and which mode), which catches “I copied the config from the direct endpoint section by mistake” quickly.

Terminal window
PGPASSWORD='<dashboard password>' psql \
-h 127.0.0.1 -p 15432 \
-U postgres.abcdefghijkl -d postgres \
-c 'select 1'

Note Querycop’s LISTEN_PORT is unchanged — Querycop terminates client connections on its own listener regardless of which Supabase endpoint it’s proxying to. Only the backend-side host / port / client username change.

WorkloadEndpoint
Stateless HTTP request handlers (Node, Go, Python, Rails)Pooler 6543 (transaction mode)
Workers using LISTEN / NOTIFYDirect
Migration tools (Alembic, dbmate, golang-migrate)Direct (need session state for transactions, advisory locks)
psql interactive sessionsDirect (more familiar semantics)

If your app needs both, run two Querycop processes — one per endpoint — and point the respective client pools at each. See the Aurora multi-instance pattern for the operational shape; SQL-aware routing is deferred to a future epic.

Terminal window
# Terminal 1: bring up Querycop with the env above.
docker compose up -d # or `querycop` directly
# Terminal 2: connect via psql.
PGPASSWORD='<password>' psql \
-h 127.0.0.1 -p 15432 \
-U postgres -d postgres \
-c 'select 1'
# ?column?
# ----------
# 1
# (1 row)

A green select 1 means proxy-side TLS and the backend TLS both worked. If you see something else, the most common first-time-setup surfaces are:

  1. backend TLS negotiation failed: x509: certificate signed by unknown authority → CA file not the one Supabase is using for this project. Re-download from the dashboard (the file ships with a date-suffixed filename; older projects use a different chain).
  2. password authentication failed for user "postgres" (pooler endpoint) → forgot to suffix the username with .<project-ref>.
  3. FATAL: Tenant or user not found (pooler endpoint, on Supavisor logs) → same: the .<project-ref> suffix is missing or misspelled.

Pooler username format is project-ref-suffixed

Section titled “Pooler username format is project-ref-suffixed”

This is the defining first-setup stumble specific to Supabase. The pooler expects <role>.<project-ref> as the username, not bare <role>. Querycop forwards the username unchanged from the client’s StartupMessage, so this lands as a client-side config change:

Terminal window
# Direct endpoint
-U postgres
# Pooler endpoint
-U postgres.abcdefghijkl

The same applies to non-default roles you create yourself: app_user.abcdefghijkl on the pooler.

Transaction-mode pooling drops session-bound state

Section titled “Transaction-mode pooling drops session-bound state”

If you point Querycop at the pooler on port 6543 and your client relies on any of:

  • Server-side prepared statements (PREPARE / EXECUTE, or driver-level extended query protocol with name)
  • LISTEN / NOTIFY
  • Session GUCs (SET search_path, SET ROLE)
  • Temp tables that persist across BEGIN/COMMIT boundaries
  • Advisory locks (pg_advisory_lock)

…they will silently misbehave. The transaction-mode pooler returns the underlying connection to the pool at every COMMIT / ROLLBACK, so anything bound to a session lifecycle is lost.

Workarounds:

  • Use the session-mode port (5432) on the pooler if you want pooled connections but session semantics.
  • Use the direct endpoint if you have stateful workloads.
  • Disable prepared-statement caching in your driver (e.g. prepareThreshold=0 in pgjdbc, prefer_simple_protocol=true in pgx).

Querycop doesn’t surface “this query needs session state” — that diagnosis happens client-side.

Direct endpoint is IPv6-only on the default plan

Section titled “Direct endpoint is IPv6-only on the default plan”

Supabase moved the direct endpoint (db.<project-ref>.supabase.co) to IPv6-only by default. The pooler endpoint (aws-0-<region>.pooler.supabase.com) remains reachable over IPv4.

Symptom on a typical IPv4-only VM / container: Querycop logs backend dial failed: connect: network is unreachable (or no route to host) for every connection attempt to the direct endpoint, before TLS even starts. The DNS resolution itself returns only an AAAA record; nothing connects.

You have three options, in rough order of preference:

  1. Switch to the Supavisor pooler endpoint (Pattern B) if your workload is compatible with transaction-mode pooling. This is often the cheapest fix because the pooler is reachable over IPv4 without any add-on, and it scales connections better anyway.
  2. Enable the project’s IPv4 add-on (a paid add-on as of 2026-05). The direct endpoint then resolves to an IPv4 address as well, and Querycop’s IPv4-only host connects normally.
  3. Add working IPv6 to the Querycop host’s network. On GKE, that means a dual-stack cluster. On a typical Docker host, you need both the host’s outbound IPv6 and Docker’s IPv6 networking enabled (which is non-default; see Docker’s IPv6 docs).

To diagnose which path your host is taking:

Terminal window
getent hosts db.abcdefghijkl.supabase.co
# IPv6-only project (default plan):
# 2606:4700:f0::1234 db.abcdefghijkl.supabase.co
# IPv4 add-on enabled:
# 1.2.3.4 db.abcdefghijkl.supabase.co
# 2606:4700:f0::1234 db.abcdefghijkl.supabase.co

Querycop itself does nothing special for IPv6 — it uses Go’s standard net.Dial, which honors whatever the host’s resolver and routing table return. The fix is at the network layer, not in Querycop.

Supabase has rotated their root CA in recent history. The dashboard prompts when a rotation is upcoming; the practical operator pattern is:

  1. When the dashboard signals a rotation, re-download the CA file to the same path Querycop reads.
  2. Restart Querycop (no in-process hot-reload).

If you’re on a project that pre-dates the latest rotation and you see x509: certificate signed by unknown authority, that’s the dashboard re-issued the cert against a new chain and you need to refresh the local CA file.

Client→proxy TLS vs proxy→Supabase TLS are SEPARATE legs

Section titled “Client→proxy TLS vs proxy→Supabase TLS are SEPARATE legs”
LegConfigured byDefault
Client → QuerycopGATEKEEPER_PROXY_TLS_CERT / _KEYOFF — plaintext unless you put TLS material in front
Querycop → SupabaseGATEKEEPER_BACKEND_TLS_* (this page)prefer (default) — upgrade to verify-full per recipe

In this cookbook the proxy→DB leg is verify-full. The client→proxy leg is your call.

Because Supabase auth is password-based, the password traverses the client→proxy leg unchanged. Don’t run app→Querycop in plaintext over a non-loopback network unless the network itself is trusted (e.g. a service-mesh-encrypted in-cluster connection).

The two endpoints have very different connection-limit characteristics:

  • Direct endpoint: limited by the underlying Postgres max_connections (Supabase sets this based on your plan’s instance size — small instances default to 60-100).
  • Pooler endpoint: Supavisor multiplexes; from the client’s perspective the connection limit is much higher (thousands), but the underlying Postgres connections are still capped — Supavisor queues your client connections if it can’t get an upstream slot.

If you see error: sorry, too many clients already from the direct endpoint, the fix is to either bump the plan or move that workload to the pooler. If you see slow connect / connection refused from the pooler, it’s usually upstream saturation in Supavisor, not a Querycop issue — check the project’s database health in the dashboard.

Supabase doesn’t expose programmatic password rotation in the PostgreSQL data path — passwords are managed through the dashboard. Querycop forwards the client’s password unchanged; rotating means:

  1. Update the password via the dashboard.
  2. Update your client’s connection string / secrets store.
  3. Recycle the app’s connection pool.

Querycop itself holds no password state, so there’s nothing to re-roll on the Querycop side.