PlanetScale Community
Status: tested against
querycopHEAD on 2026-05-18. Endpoint conventions cited as of 2026-05.
PlanetScale is a managed Vitess-on-MySQL service. From the wire-protocol standpoint it speaks MySQL; from the operational standpoint it differs from RDS / Cloud SQL MySQL in three ways that matter to a Querycop deployment:
- Auth is per-branch passwords, not MySQL
CREATE USERcalls. For each branch you want to connect to, you generate a(username, password)pair from the dashboard (Branch → Connect → New password); that pair is what the client puts in its connection string. - TLS is mandatory at the server side. PlanetScale rejects
plaintext connections — Querycop’s backend leg MUST be one of
require/verify-ca/verify-full. - Vitess semantics: no foreign-key enforcement unless explicitly
enabled, no
LOAD DATA INFILE, and some session-bound features differ from stock MySQL. These are not Querycop-specific but worth knowing when you start sending traffic through.
PlanetScale does not currently expose an IAM / OAuth equivalent for MySQL authentication — auth is the branch-password pair — so the IAM auth section is intentionally omitted from this page.
Side note on PlanetScale service tokens. A separate concept, service tokens, is sometimes confused with branch passwords. Service tokens are credentials for the PlanetScale REST API (creating branches, generating passwords, opening deploy requests, etc.) — they authenticate automation scripts to the control plane, not MySQL clients to the data plane. If you’re automating password issuance from CI, you’d use a service token to call the API that generates the branch password Querycop ultimately forwards. The Querycop-facing config on this page only uses branch passwords; service tokens never touch the MySQL wire.
For a PlanetScale branch with a generated branch password, the production-ready config is:
| Env var | Value |
|---|---|
GATEKEEPER_BACKEND_HOST | aws.connect.psdb.cloud (or gcp.connect.psdb.cloud, depending on region) |
GATEKEEPER_BACKEND_PORT | 3306 |
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) |
GATEKEEPER_BACKEND_POOLER | vitess (observability-only — PlanetScale routes every connection through Vitess) |
The connection routes to the correct branch via the branch-password username the client sends in its handshake — when you generate a password from the dashboard for a specific branch, the username embeds the branch routing. Querycop forwards the username unchanged, so the routing Just Works.
⚠️ MySQL protocol coverage in Querycop is partial. Same caveat as rds-mysql.md — Querycop’s policy / risk-scoring covers the text protocol (
COM_QUERY); server-side prepared statements pass through but are not parsed for risk-scoring. Seedocs/known-limitations.md.
Prerequisites
Section titled “Prerequisites”-
A PlanetScale database with at least one branch. The connection region (AWS vs GCP) is set per-database at creation time.
-
A branch password generated from the dashboard (Database → <branch> → Connect → New password) with the appropriate role (e.g.
Reader,Writer,Admin,Read/Write) for what your client needs to do. The dashboard shows the(username, password)pair exactly once on creation; copy it into your secrets store at that moment, because there’s no retrieve-later flow — lose it and you generate a new one. -
Network reachability from the Querycop host to
<region>.connect.psdb.cloud:3306over the public internet. No VPC peering on the standard tier. -
An up-to-date system root CA bundle on the Querycop host on disk at a known path. PlanetScale’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
-
Querycop with backend TLS support (
GATEKEEPER_BACKEND_TLS_*for MySQL shipped there).
Branch-password auth
Section titled “Branch-password auth”PlanetScale’s only data-plane auth is the branch-password
(username + password) pair you generate from the dashboard.
Querycop forwards both 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”PlanetScale’s *.connect.psdb.cloud endpoints serve Let’s Encrypt-
issued certs. The OS root bundle that ships with the Querycop
container already contains ISRG Root X1 / X2 and verifies the chain
— no PlanetScale-specific bundle to download.
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 change), download just ISRG Root X1 and point at that instead:
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.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 regional connect endpoint matching your databaseexport GATEKEEPER_BACKEND_HOST=aws.connect.psdb.cloud # or gcp.connect.psdb.cloudexport GATEKEEPER_BACKEND_PORT=3306
# 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 unset — Querycop derives it from BACKEND_HOST, which# matches the SAN on the Let's Encrypt cert.
# Observability-only topology hint. PlanetScale puts every# connection through Vitess, so always set this when proxying to# *.connect.psdb.cloud. It doesn't change wire behavior — see the# Vitess gotcha section below for what actually differs from stock MySQL.export GATEKEEPER_BACKEND_POOLER=vitess
# Standard Querycop runtimeexport GATEKEEPER_LISTEN_PORT=15336export GATEKEEPER_API_PORT=8080export ADMIN_API_KEY=$(openssl rand -hex 16)
querycopStep 3: Connect the client
Section titled “Step 3: Connect the client”The branch-password (username, password) pair from the dashboard
both look like random strings — neither resembles a traditional
MySQL user. The username embeds the branch routing PlanetScale uses
to figure out which database/branch to forward to.
mysql -h 127.0.0.1 -P 15336 \ -u '<dashboard-username>' \ -p'<dashboard-password>' \ appdb# Or via MYSQL_PWD env if the client refuses inline -pMYSQL_PWD='<dashboard-password>' mysql \ -h 127.0.0.1 -P 15336 -u '<dashboard-username>' appdbverify-full is the right default; PlanetScale also refuses any
mode weaker than require server-side, so falling back to disable
is not even an option — the backend handshake fails immediately.
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 mysql.MYSQL_PWD='<dashboard-password>' mysql \ -h 127.0.0.1 -P 15336 \ -u '<dashboard-username>' \ appdb \ -e 'select 1'# +---+# | 1 |# +---+# | 1 |# +---+A green select 1 means proxy-side handshake, backend TLS, and
branch-password auth all worked. If you see something else:
ERROR 2026 (HY000): SSL connection error: certificate verify failed→ root CA bundle issue (stale Alpine image, missing ca-certificates package). Switch to a base image with current roots.ERROR 1045 (28000): Access denied for user '…'→ branch password wrong / expired / generated on a different branch than the one your client is targeting. Re-issue from the dashboard on the right branch.ERROR 2003 (HY000): Can't connect to MySQL server on 'aws.connect.psdb.cloud:3306'→ outbound network problem (proxy, firewall, IPv6 routing). PlanetScale’s endpoint resolves IPv4 + IPv6 in most regions; a half-broken IPv6 path picks the unreachable family.
Gotchas
Section titled “Gotchas”Branch passwords are scoped to one branch + one role
Section titled “Branch passwords are scoped to one branch + one role”Each branch password is generated against:
- One specific branch (
main,staging, a feature branch, etc.) - One role (
Reader,Writer,Read/Write,Admin)
The branch is baked into the username at generation time, so a
password generated for staging cannot be used to reach main —
PlanetScale’s edge routing dispatches by username. Likewise, a
Reader password cannot run INSERT even if it reaches the right
branch.
When you cut over a workload to a new branch (e.g. merge a feature
branch into main via a deploy request and start sending production
traffic to it), you generate a fresh password for that branch and
roll the credentials into the client’s secrets store. Access Denied
on the client side with no Querycop-side signal usually means the
password was generated for a branch that no longer exists, or for a
role that lacks the privilege the query needs.
Vitess semantics differ from stock MySQL
Section titled “Vitess semantics differ from stock MySQL”PlanetScale runs Vitess in front of the underlying MySQL shards. Some behaviors differ:
| Feature | PlanetScale (Vitess) | Stock MySQL |
|---|---|---|
| Foreign-key constraints | OFF by default; can be enabled per database (database-level setting, not per-branch) | ON, enforced |
LOAD DATA INFILE | Not supported | Supported |
| Cross-shard transactions | Atomic only within a single shard | Cluster-wide possible |
| Server-side cursors | Limited | Full |
INFORMATION_SCHEMA queries | Routed via Vitess; some columns differ | Direct |
| Schema changes | Via deploy requests (dashboard), not direct ALTER | Direct ALTER |
This is not Querycop-related — Querycop forwards queries unchanged — but if you’re moving an existing app onto PlanetScale and seeing unexpected behavior, the Vitess layer is the more likely culprit than Querycop.
Server-side prepared statements bypass risk-scoring
Section titled “Server-side prepared statements bypass risk-scoring”Same caveat as RDS MySQL: Querycop’s policy / risk-scoring engine
covers the text protocol (COM_QUERY). Server-side prepared
statements (COM_STMT_PREPARE / _EXECUTE) pass through without
risk-scoring. Drivers that use them by default include JDBC
(useServerPrepStmts=true) and PHP mysqli prepared bindings.
If you rely on Querycop for guard-rails:
- Disable server-side prepares in your driver, OR
- Accept the coverage gap on prepared paths, OR
- Read
docs/known-limitations.mdand decide based on the full list.
PlanetScale-specific note: Vitess’s prepared-statement support has some additional caveats around routing-key extraction that can cause prepared queries to fail in PlanetScale even when they’d work in stock MySQL — this is a Vitess limitation, not a Querycop one.
Client→proxy TLS vs proxy→PlanetScale TLS are SEPARATE legs
Section titled “Client→proxy TLS vs proxy→PlanetScale 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 → PlanetScale | 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 PlanetScale auth is the branch password, that password traverses the client→proxy leg unchanged (Querycop forwards it). Don’t run app→Querycop in plaintext over a non-loopback network unless the network itself is trusted (service-mesh-encrypted, etc.).
Connection limits scale with plan
Section titled “Connection limits scale with plan”PlanetScale enforces a per-database connection limit that varies by plan. The pooled-connection layer absorbs some bursting, but if your app holds many idle connections through Querycop you can hit the ceiling. Symptoms:
ERROR 1040 (HY000): Too many connectionsfrom the backend side.- Slow / queued connect attempts during burst.
Mitigations:
- Reduce idle-connection pool size in the app.
- Move to a higher plan if sustained.
- Querycop doesn’t add or remove the limit — it just forwards.
IPv6 / dual-stack
Section titled “IPv6 / dual-stack”PlanetScale’s *.connect.psdb.cloud endpoints resolve both A and
AAAA records in most regions. If the Querycop host has broken
IPv6 routing (a common DNS64 / NAT64 gotcha), connections can
intermittently fail depending on which address family Go’s resolver
picks.
Workarounds:
GODEBUG=netdns=go+v4on the Querycop process forces IPv4.- Fix the IPv6 routing if you can — PlanetScale’s IPv6 path is a peer of the IPv4 path, not a degraded sibling.
Branch-password rotation
Section titled “Branch-password rotation”PlanetScale doesn’t expose programmatic password rotation in the
data plane (no ALTER USER … IDENTIFIED BY … flow). Rotation
means:
- Generate a new branch password from the dashboard.
- Roll the new
(username, password)pair into your client’s secret store. - Recycle the app’s connection pool.
- Delete the old password from the dashboard once you’ve confirmed nothing is using it.
If you’re automating this from CI / a job, you’d use a PlanetScale service token (the control-plane API credential mentioned in the side note at the top of this page) to call the REST API endpoint that generates the new branch password. Querycop only ever sees the branch password the API returned and the client subsequently presents — service tokens never reach the MySQL wire.
Querycop holds no branch-password state of its own — it just forwards whatever the client sent.
Cross-links
Section titled “Cross-links”docs/configuration.md§1.5 — backend TLS reference (MySQL supported)docs/known-limitations.md— full MySQL protocol-coverage gap list- PlanetScale docs: Connection strings (branch passwords)
- PlanetScale docs: Service tokens (API/control-plane auth)
- PlanetScale docs: Foreign key constraints
- PlanetScale docs: Vitess and MySQL compatibility