Skip to content

PlanetScale Community

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

  1. Auth is per-branch passwords, not MySQL CREATE USER calls. 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.
  2. TLS is mandatory at the server side. PlanetScale rejects plaintext connections — Querycop’s backend leg MUST be one of require / verify-ca / verify-full.
  3. 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 varValue
GATEKEEPER_BACKEND_HOSTaws.connect.psdb.cloud (or gcp.connect.psdb.cloud, depending on region)
GATEKEEPER_BACKEND_PORT3306
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)
GATEKEEPER_BACKEND_POOLERvitess (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. See docs/known-limitations.md.

  • 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:3306 over 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-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.

  • Querycop with backend TLS support (GATEKEEPER_BACKEND_TLS_* for MySQL shipped there).

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:

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 change), download just ISRG Root X1 and point at that instead:

Terminal window
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 regional connect endpoint matching your database
export GATEKEEPER_BACKEND_HOST=aws.connect.psdb.cloud # or gcp.connect.psdb.cloud
export 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-full
export 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 runtime
export GATEKEEPER_LISTEN_PORT=15336
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop

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.

Terminal window
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 -p
MYSQL_PWD='<dashboard-password>' mysql \
-h 127.0.0.1 -P 15336 -u '<dashboard-username>' appdb

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

Terminal window
# 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:

  1. 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.
  2. 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.
  3. 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.

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.

PlanetScale runs Vitess in front of the underlying MySQL shards. Some behaviors differ:

FeaturePlanetScale (Vitess)Stock MySQL
Foreign-key constraintsOFF by default; can be enabled per database (database-level setting, not per-branch)ON, enforced
LOAD DATA INFILENot supportedSupported
Cross-shard transactionsAtomic only within a single shardCluster-wide possible
Server-side cursorsLimitedFull
INFORMATION_SCHEMA queriesRouted via Vitess; some columns differDirect
Schema changesVia deploy requests (dashboard), not direct ALTERDirect 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.md and 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”
LegConfigured byDefault
Client → QuerycopGATEKEEPER_PROXY_TLS_CERT / _KEYOFF — plaintext unless you put TLS material in front
Querycop → PlanetScaleGATEKEEPER_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.).

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 connections from 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.

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

PlanetScale doesn’t expose programmatic password rotation in the data plane (no ALTER USER … IDENTIFIED BY … flow). Rotation means:

  1. Generate a new branch password from the dashboard.
  2. Roll the new (username, password) pair into your client’s secret store.
  3. Recycle the app’s connection pool.
  4. 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.