Supabase Community
Status: tested against
querycopHEAD 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 var | Value |
|---|---|
GATEKEEPER_BACKEND_HOST | db.abcdefghijkl.supabase.co |
GATEKEEPER_BACKEND_PORT | 5432 |
GATEKEEPER_BACKEND_TLS_MODE | verify-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.coresolves 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.
Prerequisites
Section titled “Prerequisites”- 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
postgresuser’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(forpg_notify-driven workflows)- Session GUCs (
SET search_path,SET ROLE) - Temp tables that survive across multiple statements
Step 1: Download the Supabase CA
Section titled “Step 1: Download the Supabase CA”# 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-checkgrep -c BEGIN /etc/ssl/certs/supabase-prod-ca.pem # should be ≥ 1Supabase 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.
Step 2: Configure Querycop
Section titled “Step 2: Configure Querycop”# Required: the direct endpointexport GATEKEEPER_BACKEND_HOST=db.abcdefghijkl.supabase.coexport GATEKEEPER_BACKEND_PORT=5432
# Required: full TLS verification against the per-project CAexport GATEKEEPER_BACKEND_TLS_MODE=verify-fullexport 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 runtimeexport GATEKEEPER_LISTEN_PORT=15432export GATEKEEPER_API_PORT=8080export ADMIN_API_KEY=$(openssl rand -hex 16)
querycopStep 3: Connect the client
Section titled “Step 3: Connect the client”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.
Pattern B — Supavisor pooler endpoint
Section titled “Pattern B — Supavisor pooler endpoint”The pooler endpoint serves a different operational tradeoff:
| Pooler port | Mode | Use for |
|---|---|---|
6543 | Transaction | High request rate, short queries, no session-bound state. Most HTTP app servers. |
5432 | Session | Like a direct connection, but with the pooler’s load-balancing in front. Session-bound state works. |
Username format change
Section titled “Username format change”The pooler requires the client-side username to include the project ref:
direct endpoint: -U postgrespooler endpoint: -U postgres.abcdefghijkl └─ DB role └─ project refThe 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.
Querycop config for the pooler
Section titled “Querycop config for the pooler”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-fullexport GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/supabase-prod-ca.pem# SERVER_NAME derived from BACKEND_HOST (pooler hostname).
export GATEKEEPER_LISTEN_PORT=15432export GATEKEEPER_API_PORT=8080export ADMIN_API_KEY=$(openssl rand -hex 16)
querycopGATEKEEPER_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.
Client connection
Section titled “Client connection”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.
Choosing direct vs pooler
Section titled “Choosing direct vs pooler”| Workload | Endpoint |
|---|---|
| Stateless HTTP request handlers (Node, Go, Python, Rails) | Pooler 6543 (transaction mode) |
Workers using LISTEN / NOTIFY | Direct |
| Migration tools (Alembic, dbmate, golang-migrate) | Direct (need session state for transactions, advisory locks) |
psql interactive sessions | Direct (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.
Smoke test
Section titled “Smoke test”# 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:
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).password authentication failed for user "postgres"(pooler endpoint) → forgot to suffix the username with.<project-ref>.FATAL: Tenant or user not found(pooler endpoint, on Supavisor logs) → same: the.<project-ref>suffix is missing or misspelled.
Gotchas
Section titled “Gotchas”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:
# Direct endpoint-U postgres
# Pooler endpoint-U postgres.abcdefghijklThe 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-levelextended query protocol with name) LISTEN/NOTIFY- Session GUCs (
SET search_path,SET ROLE) - Temp tables that persist across
BEGIN/COMMITboundaries - 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=0inpgjdbc,prefer_simple_protocol=trueinpgx).
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:
- 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.
- 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.
- 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:
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.coQuerycop 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 CA rotation
Section titled “Supabase CA rotation”Supabase has rotated their root CA in recent history. The dashboard prompts when a rotation is upcoming; the practical operator pattern is:
- When the dashboard signals a rotation, re-download the CA file to the same path Querycop reads.
- 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”| Leg | Configured by | Default |
|---|---|---|
| Client → Querycop | GATEKEEPER_PROXY_TLS_CERT / _KEY | OFF — plaintext unless you put TLS material in front |
| Querycop → Supabase | GATEKEEPER_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).
Connection limits differ by endpoint
Section titled “Connection limits differ by endpoint”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.
Password rotation
Section titled “Password rotation”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:
- Update the password via the dashboard.
- Update your client’s connection string / secrets store.
- Recycle the app’s connection pool.
Querycop itself holds no password state, so there’s nothing to re-roll on the Querycop side.
Cross-links
Section titled “Cross-links”docs/configuration.md§1.5 — backend TLS reference- Supabase docs: Connection pooling with Supavisor
- Supabase docs: SSL connections
- Supavisor GitHub: supabase/supavisor