Preview branches per PR
PlanetScale’s branch model maps directly onto alchemy stages: the database is the expensive, long-lived tier; branches are cheap forks. Split your stages the same way and every PR gets an isolated schema and data set without provisioning a cluster per preview:
staging-*stages own the long-lived database.pr-*stages reference that database and own only the ephemeral pieces — a branch, a credential, and compute.
The pattern is identical for both engines; only the resource names and the credential model differ. New here? Set up credentials first.
Read the active stage
Section titled “Read the active stage”The create-vs-reference decision keys off the stage being deployed,
which the Stack service exposes:
import * as Alchemy from "alchemy";
const { stage } = yield* Alchemy.Stack;stage is whatever was passed to alchemy deploy --stage <name> —
dev_sam, staging-pr-42, or pr-42.
Create or reference the database
Section titled “Create or reference the database”PR stages reference the database with PostgresDatabase.ref; every
other stage creates it:
import * as Planetscale from "alchemy/Planetscale";
const database = stage.startsWith("pr-") ? yield* Planetscale.PostgresDatabase.ref("app-db", { stage: `staging-${stage}`, }) : yield* Planetscale.PostgresDatabase("app-db", { region: { slug: "us-east" }, clusterSize: "PS_10", });Resource.ref(id, { stage }) reads the deployed resource’s
attributes from another stage’s state at plan time — the lookup is
keyed by { stack, stage, id }, so the logical ID "app-db" must
match what the owning stage created. database is typed
Planetscale.PostgresDatabase on both paths, and downstream
resources don’t care which one ran. Note the ref targets
staging-${stage}, not a global "staging": pr-42 reads from a
parallel staging-pr-42 stage, so each PR’s data lives in its own
database. Prefer one database shared by every PR? Use
stage: "staging".
A branch per stage
Section titled “A branch per stage”Below the database line, nothing is conditional. Every stage — including each PR stage — forks its own branch:
const branch = yield* Planetscale.PostgresBranch("app-branch", { database, migrationsDir: "./migrations",});Branches fork from main by default and carry their own
migrationsDir: pending .sql files are applied to the branch on
every deploy, so each preview migrates itself independently — see
Migrations for ordering, hashing, and the
tracking table. To fork data as well as schema, seedData: "last_successful_backup" restores the last backup into the new
branch.
Mint a per-stage credential
Section titled “Mint a per-stage credential”The credential is scoped to the branch, so it’s per-stage too:
const role = yield* Planetscale.PostgresRole("app-role", { database, branch, inheritedRoles: ["postgres"],});role.origin (direct) and role.pooledOrigin (PSBouncer) are what
you hand to the runtime consumer — see
Postgres for the role model and the
direct-vs-pooled distinction.
The MySQL variant
Section titled “The MySQL variant”The MySQL (Vitess) family follows the same two tiers with its own
resource trio — MySQLDatabase.ref, a non-production MySQLBranch,
and a MySQLPassword in place of the Postgres role:
const database = stage.startsWith("pr-") ? yield* Planetscale.MySQLDatabase.ref("app-db", { stage: `staging-${stage}`, }) : yield* Planetscale.MySQLDatabase("app-db", { region: { slug: "us-east" }, clusterSize: "PS_10", });
const branch = yield* Planetscale.MySQLBranch("app-branch", { database, isProduction: false, migrationsDir: "./migrations",});
const password = yield* Planetscale.MySQLPassword("app-password", { database, branch, role: "readwriter",});Non-production branches run on the PS_DEV size, and the password’s
role is a Vitess access level ("reader", "writer", "admin",
"readwriter") rather than inherited Postgres roles. Migrations
behave slightly differently on Vitess — DDL commits implicitly, so
prefer one schema change per file — see the engine differences in
Migrations.
Deploy the owner first
Section titled “Deploy the owner first”A ref can only resolve against state that exists, so the owning stage deploys before the PR stage that references it:
alchemy deploy --stage staging-pr-42 # creates the databasealchemy deploy --stage pr-42 # references it, forks a branchDeploying pr-42 first fails at plan time with
InvalidReferenceError — there’s nothing under
{ stack, stage: "staging-pr-42", id: "app-db" } to read yet.
Tear down
Section titled “Tear down”alchemy destroy --stage pr-42Destroy removes the per-stage resources — branch, credential, compute — but leaves the referenced database untouched: the PR stage doesn’t own it, so it can’t delete it.
Wire it into your runtime
Section titled “Wire it into your runtime”The snippets above end at a credential. Connecting it to compute is runtime-specific:
- Shared database across stages
— the general
Resource.refpattern, step by step. - Branch from a shared database — the full walkthrough of this pattern with a Cloudflare Worker on the other end.
- Hyperdrive —
role.origin/password.originplug straight into aCloudflare.Hyperdrive.Connection.
Complete working projects:
cloudflare-planetscale-postgres-drizzle
and
cloudflare-planetscale-mysql-drizzle.
Where next
Section titled “Where next”- Drizzle ORM with PlanetScale — generate the migrations each preview branch applies.
- Migrations — how
migrationsDirfiles are ordered, hashed, and tracked. - Postgres and MySQL — the two resource families used above.