Neon Community
Status: tested against
querycopHEAD on 2026-05-18. Endpoint conventions cited as of 2026-05.
Neon is a serverless PostgreSQL platform that separates compute (auto-suspending endpoints) from storage. Every project gets one or more branches, each with one or more compute endpoints. The endpoint hostname encodes the endpoint ID and is the unit you point Querycop at.
This page covers:
- Neon direct endpoint (e.g.
ep-cool-leaf-123456.us-east-2.aws.neon.tech) - Neon pooler endpoint (the same host with a
-poolersuffix)
Neon does not currently expose an IAM / OAuth equivalent for
PostgreSQL PasswordMessage injection — auth is password-based — so
the IAM auth section is intentionally omitted from this page.
For a Neon direct endpoint with password auth, the production-ready config is:
| Env var | Value |
|---|---|
GATEKEEPER_BACKEND_HOST | ep-cool-leaf-123456.us-east-2.aws.neon.tech |
GATEKEEPER_BACKEND_PORT | 5432 |
GATEKEEPER_BACKEND_TLS_MODE | verify-full |
GATEKEEPER_BACKEND_TLS_CA_FILE | /etc/ssl/certs/ca-certificates.crt (the host’s OS root bundle — required even when Querycop ends up validating against it) |
GATEKEEPER_BACKEND_TLS_SERVER_NAME | (unset — derived from BACKEND_HOST, which Neon uses for SNI-based routing) |
For the pooler endpoint, swap the host suffix:
ep-cool-leaf-123456-pooler.us-east-2.aws.neon.tech. Pooler-mode
constraints are covered in Gotchas.
Prerequisites
Section titled “Prerequisites”-
A Neon project with at least one branch and one compute endpoint.
-
The endpoint’s connection string from the Neon dashboard (Project → Connection Details). The host, role, and database name are all visible there.
-
The OS where Querycop runs has an up-to-date system root CA bundle on disk at a known path. Neon’s endpoints serve Let’s Encrypt- issued certs, which chain to ISRG Root X1 / X2, so any current distro bundle works. Typical paths:
- Debian / Ubuntu / the
debian:trixie-slim-based Querycop runtime image:/etc/ssl/certs/ca-certificates.crt - Alpine (after
apk add ca-certificates):/etc/ssl/certs/ca-certificates.crt - RHEL / Fedora / Amazon Linux:
/etc/pki/tls/certs/ca-bundle.crt
Querycop’s
verify-ca/verify-fullmodes require an explicitGATEKEEPER_BACKEND_TLS_CA_FILEpath — the implicit-system-pool fallback is deliberately not supported, so an explicit path needs to land in the env even when the file you’re pointing at IS the OS root bundle. The startup check fails fast with a clear message if the env var is empty. - Debian / Ubuntu / the
-
Network reachability from the Querycop host to port 5432 on the Neon endpoint. Neon endpoints are reached over the public internet via TLS; no VPC peering required for the standard tier.
-
Querycop with backend TLS support (
GATEKEEPER_BACKEND_TLS_*).
Password auth
Section titled “Password auth”Neon issues a per-role password from the dashboard (Settings → Roles). Querycop forwards the client’s password unchanged.
Step 1: Point BACKEND_TLS_CA_FILE at the OS root bundle
Section titled “Step 1: Point BACKEND_TLS_CA_FILE at the OS root bundle”Neon’s endpoint certs are publicly-issued Let’s Encrypt certs. Unlike RDS or Cloud SQL, there is no Neon-specific CA bundle to download — the OS root bundle that ships with the Querycop container already contains ISRG Root X1 / X2 and verifies the chain.
Querycop’s verify-ca / verify-full modes require
GATEKEEPER_BACKEND_TLS_CA_FILE to be set explicitly (the implicit
system-pool fallback is deliberately not supported — see
backend_tls.go validation). For the
Querycop runtime container (Debian-based) point it at the OS bundle:
export GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/ca-certificates.crtOn Alpine the same path works after apk add ca-certificates. On
RHEL / Fedora / Amazon Linux, use
/etc/pki/tls/certs/ca-bundle.crt.
If you want to pin tighter than the OS bundle (defense-in-depth against a future Let’s Encrypt issuer rotation surprising your deployment), download just ISRG Root X1 and point at that instead:
# Optional: pin to ISRG Root X1 explicitlycurl -fsSL https://letsencrypt.org/certs/isrgrootx1.pem \ -o /etc/ssl/certs/letsencrypt-isrg-root-x1.pem
export GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/letsencrypt-isrg-root-x1.pemMost operators don’t need the pinned form; the OS bundle is fine.
Step 2: Configure Querycop
Section titled “Step 2: Configure Querycop”# Required: the Neon endpoint (this hostname doubles as the SNI label# Neon uses to route to the correct compute — see Gotchas).export GATEKEEPER_BACKEND_HOST=ep-cool-leaf-123456.us-east-2.aws.neon.techexport GATEKEEPER_BACKEND_PORT=5432
# Required: full TLS verification against the OS root bundle.# CA_FILE must be set explicitly — Querycop rejects verify-ca /# verify-full at startup if it's empty.export GATEKEEPER_BACKEND_TLS_MODE=verify-fullexport GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/ca-certificates.crt# SERVER_NAME explicitly NOT set — Querycop derives it from# BACKEND_HOST, and that hostname must be the SNI value Neon# expects in order to route to the correct compute 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”psql -h 127.0.0.1 -p 15432 -U <project-owner> -d <db> -W# Password prompt → enter the per-role password from the Neon dashboardverify-full is the right default. require (no certificate
verification) is evaluation-only: when you do that on Neon you not
only skip cert validation but also leave yourself open to a
silent re-routing if the SNI hint is ever ignored by an upstream.
disable is not an appropriate choice — Neon endpoints are reached
over the public internet, and disable would put the password on the
open wire.
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 <project-owner> -d <db> \ -c 'select 1'# ?column?# ----------# 1# (1 row)A green select 1 means proxy-side TLS, the SNI-based routing, and
the backend TLS all worked. If you see something else, the most
common first-time-setup surfaces are:
backend TLS negotiation failed: connection reset by peer→ SNI mismatch. The hostname Querycop is sending as SNI does not match what Neon expects. Double-check thatBACKEND_HOSTexactly matches the endpoint hostname from the dashboard (case-sensitive, no protocol prefix, no path).backend TLS negotiation failed: x509: certificate signed by unknown authority→ the container’s root CA bundle is stale. Use a base image with up-to-date roots (golang:1.25/debian:trixie-slimare fine; very oldalpine:3.10is not).password authentication failed→ wrong per-role password from the Neon dashboard.
Gotchas
Section titled “Gotchas”Neon uses SNI to route to the correct compute endpoint
Section titled “Neon uses SNI to route to the correct compute endpoint”This is the defining quirk. Neon multiplexes many compute endpoints behind a small set of regional IP addresses; the routing layer inspects the TLS SNI extension sent by the client during the handshake to pick which compute to forward to.
Consequence: the TLS handshake itself must include the endpoint hostname as SNI. If SNI is missing or set to a different hostname, Neon either rejects the connection or routes it to the wrong place.
Querycop derives SNI from BACKEND_HOST automatically (via the
standard tls.Config.ServerName field), so as long as BACKEND_HOST
is set to the endpoint hostname (ep-cool-leaf-…neon.tech), this
Just Works. Do not set BACKEND_HOST to an IP literal and rely on
BACKEND_TLS_SERVER_NAME to “fix” the SNI — Querycop fail-closes on
IP-literal hosts with verify-full. Always use the
hostname Neon gave you.
For the pooler endpoint, use the -pooler-suffixed hostname; the
pooler is a distinct compute and its hostname is a distinct SNI label.
Pooler endpoint trades PG features for connection density
Section titled “Pooler endpoint trades PG features for connection density”Neon offers a PgBouncer-backed pooler endpoint
at the <endpoint-id>-pooler.… hostname. It runs in transaction
pooling mode, which means most session-bound state does NOT
survive across statements:
LISTEN/NOTIFYdoesn’t reliably deliver (the connection that receives the notification may not be the connection holding the listener)- Session-level GUCs (
SET search_path,SET ROLE) don’t persist past aCOMMIT - Temp tables get dropped at end of transaction
- Advisory locks (
pg_advisory_lock) don’t persist past aCOMMIT
There’s one nuance worth calling out specifically because it trips many drivers: prepared statements.
| Form | Pooler support |
|---|---|
SQL-level PREPARE name AS … / EXECUTE name | ❌ Not supported — the PREPARE is bound to the upstream session, which the pooler hands to another client at COMMIT. |
Protocol-level (extended query protocol, e.g. pgx’s default, JDBC prepareThreshold>0, psycopg3 named statements) | ✅ Supported by Neon’s pooler. PgBouncer ≥ 1.21 added explicit support for protocol-level named prepared statements in transaction mode, and Neon’s pooler runs a version that includes this. |
So a Go app using pgx with the default protocol-level prepared
statements works fine against the pooler; a Rails app using
PREPARE/EXECUTE via raw SQL does not. Check your driver before
ruling out the pooler purely on “prepared statements”.
If your app needs the SQL-level form, LISTEN/NOTIFY, advisory
locks, or other session-bound features, point Querycop at the
direct endpoint, not the pooler. Otherwise the pooler is the
right choice for most stateless HTTP workloads and Querycop relays
it transparently.
You can run two Querycop processes if you need both — one for the direct endpoint (app’s interactive / migration connections) and one for the pooler (high-throughput request path). The cookbook’s Aurora multi-instance pattern documents the operational shape; SQL-aware splitting is deferred to a future epic.
When the Querycop instance is pointed at the pooler endpoint, set
GATEKEEPER_BACKEND_POOLER=pgbouncer-txn so the startup log records
the topology. The pooler awareness flag is observability-only — Querycop still relays
client SQL unchanged — but the log line gives operators a fast
sanity-check that the deployment matches the cookbook recipe.
Auto-suspend means the first connection after idle is slow
Section titled “Auto-suspend means the first connection after idle is slow”Neon compute endpoints auto-suspend after a configurable idle period (default 5 min on the free tier, configurable on paid). The first client connection after suspend triggers a cold-start that can take several hundred milliseconds to a few seconds.
For Querycop, this looks like:
- Healthy steady-state traffic: connections through Querycop have Neon-equivalent latency.
- After idle: the next client connect spends extra time inside the TLS handshake / startup-message round-trip while Neon spins compute back up.
Querycop itself doesn’t add anything special for cold-start; it just propagates the latency. If you have strict tail-latency SLOs, disable auto-suspend on the endpoint (paid plan) or schedule a periodic keep-alive probe.
Client→proxy TLS vs proxy→Neon TLS are SEPARATE legs
Section titled “Client→proxy TLS vs proxy→Neon 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 → Neon | 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 — psql over plaintext to a localhost proxy is fine
for development; for production put a TLS cert on Querycop
(GATEKEEPER_PROXY_TLS_CERT / _KEY).
The Neon password traverses the client→proxy leg (Querycop forwards
the client’s PasswordMessage unchanged), so unlike the IAM-token
recipes for AWS / GCP, the client-to-proxy TLS leg is meaningful for
password confidentiality — don’t run app→Querycop in plaintext over a
non-loopback network.
IPv6 / dual-stack behavior
Section titled “IPv6 / dual-stack behavior”Neon’s endpoint hostnames resolve to A and AAAA records in most
regions. Go’s net.Dial picks an address family based on system
config. If the Querycop host has broken IPv6 routing (a common DNS64
/ NAT64 gotcha), you’ll see connect: network is unreachable for
some connections and not others, depending on which family Go picked.
Workarounds:
- Set
GODEBUG=netdns=go+v4on the Querycop process to force the Go resolver and prefer IPv4. - Fix the IPv6 routing (preferred — Neon’s IPv6 path is a peer of the IPv4 path, and you’ll get worse availability without it).
Connection-string-only auth is the Neon norm
Section titled “Connection-string-only auth is the Neon norm”Neon doesn’t expose IAM auth or ALTER USER … VALID UNTIL token
rotation in the user-facing surface. The password is rotated by
re-issuing it from the dashboard; there’s no zero-touch credential
rotation story. If you need rotation:
- Rotate from the dashboard on whatever cadence your security policy requires.
- Roll the new password into the client’s connection string (or your secrets manager).
- Querycop doesn’t store the password; it just forwards what the client sent.
Cross-links
Section titled “Cross-links”docs/configuration.md§1.5 — backend TLS reference- Neon docs: Connection pooling
- Neon docs: SSL/TLS connections
- Neon docs: Auto-suspend