Quickback Docs

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

quickback/quickback.config.ts
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 rolePurposeDeployed?
quickback_ownerNOLOGIN owner of the app and auth schemas and their objectsno
quickback_adminroutine migration actor — drizzle-kit and Better Auth migrationsno
quickbackCloudflare Worker runtime role, NOINHERIT NOBYPASSRLSyes, 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:

ValueWhere it appears
SQL rolequickbackCREATE ROLE, GRANT, ALTER ROLE, every policy's TO clause
Connection usernamequickback.<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 deploy

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

Not shared:

  • Control plane. Branch creation, canaries and the NEON_* API path are Neon-specific. quickback canary refuses a PlanetScale project even when NEON_API_KEY and NEON_PROJECT_ID are set; PlanetScale branch operations are manual today.
  • Setup output. .env.planetscale.example and docs/planetscale-postgres-setup.md replace their Neon equivalents.
  • Dependencies. No @neondatabase/serverless; the runtime is postgres (postgres.js) with drizzle-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:

FeatureStatus
Managed file storage (managed: true)rejected — the file-metadata store is a D1 database
Better Auth subscriptions pluginrejected — its schema and db acquisition are D1-only
Generated SQL triggersnot emitted on Postgres
webhooksDatabaseId / webhooksDatabaseNamerejected — 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 ROLE to a NOLOGIN role, and GRANT … WITH SET TRUE — the bootstrap grants quickback_admin and quickback membership in quickback_owner and relies on assuming it. PlanetScale documents CREATE ROLE but not membership or role assumption on a managed branch.
  • Ownership transferALTER … OWNER TO quickback_owner from the branch's setup owner to a SQL-created role.
  • SECURITY DEFINER and a pinned search_path on the generated RLS helper functions.
  • ALTER ROLE … LOGIN PASSWORD on 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

On this page