PlanetScale Postgres
Use PlanetScale Postgres with Quickback through Cloudflare Hyperdrive — roles, connection usernames, the cache-disabled binding, and named environments.
PlanetScale Postgres is the same Postgres target as Neon, on a different host. The compiler emits one Postgres implementation: the same schema, the same Better Auth wiring, the same RLS backstop, and the same migrations. For identical definitions the generated schema and security migrations are byte-identical between the two providers.
Compiler support; live acceptance pending. Every gate below is proven locally — compile, generated TypeScript, migration generation and ownership parity. The acceptance run against a real PlanetScale branch (role bootstrap, ownership transfer, restricted runtime queries, transaction rollback) has not been executed yet. Treat this page as the intended runbook rather than a support claim.
Configuration
import { defineConfig, defineRuntime, defineDatabase, defineAuth } from "@quickback/compiler";
export default defineConfig({
name: "my-app",
providers: {
runtime: defineRuntime("cloudflare"),
database: defineDatabase("planetscale-postgres", {
connectionMode: "hyperdrive",
hyperdrive: { id: "ffffffffffffffffffffffffffffffff" },
}),
auth: defineAuth("better-auth"),
},
});Hyperdrive is the only transport. Omitting connectionMode resolves to
hyperdrive; http and websocket are Neon driver modes and are rejected at
compile time, as is any runtime other than cloudflare.
planetscale — without the suffix — remains the MySQL provider name and is not
registered. Only planetscale-postgres selects this provider.
Roles: three SQL roles, one deployed
The first generated migration creates the same three roles it creates on Neon:
| SQL role | Purpose | Deployed? |
|---|---|---|
quickback_owner | NOLOGIN owner of the app and auth schemas and their objects | no |
quickback_admin | routine migration actor — drizzle-kit and Better Auth migrations | no |
quickback | Cloudflare Worker runtime role, NOINHERIT NOBYPASSRLS | yes, only this one |
They are created NOLOGIN because generated SQL cannot choose your passwords.
Enable login afterwards, and change nothing else:
ALTER ROLE quickback_admin LOGIN PASSWORD '<strong-admin-password>';
ALTER ROLE quickback LOGIN PASSWORD '<strong-password>';SQL role vs connection username
PlanetScale routes a connection to a branch by a suffix on the username. The two names are not interchangeable:
| Value | Where it appears | |
|---|---|---|
| SQL role | quickback | CREATE ROLE, GRANT, ALTER ROLE, every policy's TO clause |
| Connection username | quickback.<branch_id> | the connection string, and nowhere else |
Generated SQL never contains a branch id. If a policy or grant is written
against quickback.<branch_id>, it is wrong.
Setup owner vs migration actor
The branch's default postgres role is the setup owner. It has
BYPASSRLS, so it is exactly the role that must never run application traffic:
- Setup owner (
postgres) — runs the role-bootstrap migration once, and any later migration that changes ownership or role membership. - Migration actor (
quickback_admin) — every routine schema and security migration afterwards. - Runtime (
quickback) — the Hyperdrive connection string. Nothing else.
Cloudflare's generic PlanetScale integration guide suggests granting a driver
role pg_read_all_data and pg_write_all_data. Do not grant those to
quickback: they hand the Worker blanket table access in place of the
generated grants. (Neither bypasses RLS, so this is a least-privilege
regression rather than an isolation hole — but the generated grants are the
contract this provider is tested against.)
The Hyperdrive binding
Point Hyperdrive at the branch's direct host over TLS. Hyperdrive is the connection pool; stacking a second vendor pooler in front of it adds a hop and another place for session state to be reused.
npx wrangler hyperdrive create my-app-db \
--caching-disabled \
--connection-string="postgres://quickback.<branch_id>:<password>@<host>:5432/<database>?sslmode=verify-full"--caching-disabled is mandatory. Quickback scopes RLS per request by writing
the caller's claims into transaction-local settings; a cached result set would
be replayed to a different caller, across tenants. Caching happens at the app
layer instead.
The compiler emits the binding into wrangler.toml:
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "ffffffffffffffffffffffffffffffff"No DATABASE_URL Worker secret
Feature queries and Better Auth both read env.HYPERDRIVE.connectionString.
The Worker holds no database password.
Migration credentials are compiler/CI inputs and never Worker secrets:
drizzle-kit reads DATABASE_MIGRATION_URL (falling back to DATABASE_URL)
from .env. Copy the generated .env.planetscale.example to .env and fill
in the quickback_admin connection string.
npm run db:migrate # schema + security migrations, as quickback_admin
npx wrangler deployquickback deploy applies D1 migrations only — on any Postgres provider it
prints a warning and leaves migrations to npm run db:migrate.
Named environments
Each target owns its own branch, Hyperdrive config and Worker script, exactly
as on Neon:
one environments.<name> entry per target, each with its own
worker.bindings.hyperdrive.id, and the local dev target additionally
carrying localConnectionString. Since every environment runs the same
bundle, a target that shares another target's Hyperdrive id shares its
database — the compiler requires each one to declare its own.
What is shared with Neon, and what is not
Shared — read the Neon page for the detail, it is the same generated output:
- Worker-scoped RLS and the claim model
- Row Level Security as a backstop
- Better Auth schema generation
- Migrations and the journaled security migration
- Interactive transactions, webhooks, encryption and background jobs — all available, as on Neon Hyperdrive
Not shared:
- Control plane. Branch creation, canaries and the
NEON_*API path are Neon-specific.quickback canaryrefuses a PlanetScale project even whenNEON_API_KEYandNEON_PROJECT_IDare set; PlanetScale branch operations are manual today. - Setup output.
.env.planetscale.exampleanddocs/planetscale-postgres-setup.mdreplace their Neon equivalents. - Dependencies. No
@neondatabase/serverless; the runtime ispostgres(postgres.js) withdrizzle-orm/postgres-js.
Capability limits (fail closed)
The same gates as Neon, and for the same reason — each is a compile-time error, never silently-broken output:
| Feature | Status |
|---|---|
Managed file storage (managed: true) | rejected — the file-metadata store is a D1 database |
Better Auth subscriptions plugin | rejected — its schema and db acquisition are D1-only |
| Generated SQL triggers | not emitted on Postgres |
webhooksDatabaseId / webhooksDatabaseName | rejected — D1-only keys; webhook tables live in the webhooks schema of the same database |
connectionMode of 'http' or 'websocket' | rejected — no such driver for this provider |
Open questions for live acceptance
Recorded from the vendor documentation review (2026-09-06) so the live spike settles them rather than a reader discovering them:
SET LOCAL ROLEto aNOLOGINrole, andGRANT … WITH SET TRUE— the bootstrap grantsquickback_adminandquickbackmembership inquickback_ownerand relies on assuming it. PlanetScale documentsCREATE ROLEbut not membership or role assumption on a managed branch.- Ownership transfer —
ALTER … OWNER TO quickback_ownerfrom the branch's setup owner to a SQL-created role. SECURITY DEFINERand a pinnedsearch_pathon the generated RLS helper functions.ALTER ROLE … LOGIN PASSWORDon a SQL-created role, and whether such a role's credentials are manageable outside the console.
If any of these does not hold, it is a blocker to report — not something to work around by relaxing the runtime role.
See also
- Neon — the same Postgres implementation, in full detail
- Cloudflare D1 — the SQLite alternative
- Database providers — configuring
providers.database