Skip to content

Neon Community

Status: tested against querycop HEAD 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 -pooler suffix)

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 varValue
GATEKEEPER_BACKEND_HOSTep-cool-leaf-123456.us-east-2.aws.neon.tech
GATEKEEPER_BACKEND_PORT5432
GATEKEEPER_BACKEND_TLS_MODEverify-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.

  • 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-full modes require an explicit GATEKEEPER_BACKEND_TLS_CA_FILE path — 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.

  • 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_*).

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:

Terminal window
export GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/ca-certificates.crt

On 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:

Terminal window
# Optional: pin to ISRG Root X1 explicitly
curl -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.pem

Most operators don’t need the pinned form; the OS bundle is fine.

Terminal window
# 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.tech
export 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-full
export 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 runtime
export GATEKEEPER_LISTEN_PORT=15432
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop
Terminal window
psql -h 127.0.0.1 -p 15432 -U <project-owner> -d <db> -W
# Password prompt → enter the per-role password from the Neon dashboard

verify-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.

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 <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:

  1. 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 that BACKEND_HOST exactly matches the endpoint hostname from the dashboard (case-sensitive, no protocol prefix, no path).
  2. 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-slim are fine; very old alpine:3.10 is not).
  3. password authentication failed → wrong per-role password from the Neon dashboard.

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 / NOTIFY doesn’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 a COMMIT
  • Temp tables get dropped at end of transaction
  • Advisory locks (pg_advisory_lock) don’t persist past a COMMIT

There’s one nuance worth calling out specifically because it trips many drivers: prepared statements.

FormPooler 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”
LegConfigured byDefault
Client → QuerycopGATEKEEPER_PROXY_TLS_CERT / _KEYOFF — plaintext unless you put TLS material in front
Querycop → NeonGATEKEEPER_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.

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+v4 on 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.