1---
2title: Preview branches per PR
3description: Give every pull request its own PlanetScale branch — a long-lived database owned by a staging stage, referenced by ephemeral PR stages that fork, migrate, and tear down a branch each.
4---
5
6PlanetScale's branch model maps directly onto alchemy stages: the
7database is the expensive, long-lived tier; branches are cheap forks.
8Split your stages the same way and every PR gets an isolated schema
9and data set without provisioning a cluster per preview:
10
11- **`staging-*` stages** own the long-lived database.
12- **`pr-*` stages** reference that database and own only the
13 ephemeral pieces — a branch, a credential, and compute.
14
15The pattern is identical for both engines; only the resource names
16and the credential model differ. New here? [Set up
17credentials](/planetscale/setup) first.
18
19## Read the active stage
20
21The create-vs-reference decision keys off the stage being deployed,
22which the `Stack` service exposes:
23
24```typescript
25import * as Alchemy from "alchemy";
26
27const { stage } = yield* Alchemy.Stack;
28```
29
30`stage` is whatever was passed to `alchemy deploy --stage <name>` —
31`dev_sam`, `staging-pr-42`, or `pr-42`.
32
33## Create or reference the database
34
35PR stages reference the database with `PostgresDatabase.ref`; every
36other stage creates it:
37
38```typescript
39import * as Planetscale from "alchemy/Planetscale";
40
41const database = stage.startsWith("pr-")
42 ? yield* Planetscale.PostgresDatabase.ref("app-db", {
43 stage: `staging-${stage}`,
44 })
45 : yield* Planetscale.PostgresDatabase("app-db", {
46 region: { slug: "us-east" },
47 clusterSize: "PS_10",
48 });
49```
50
51`Resource.ref(id, { stage })` reads the deployed resource's
52attributes from another stage's state at plan time — the lookup is
53keyed by `{ stack, stage, id }`, so the logical ID `"app-db"` must
54match what the owning stage created. `database` is typed
55`Planetscale.PostgresDatabase` on both paths, and downstream
56resources don't care which one ran. Note the ref targets
57`staging-${stage}`, not a global `"staging"`: `pr-42` reads from a
58parallel `staging-pr-42` stage, so each PR's data lives in its own
59database. Prefer one database shared by every PR? Use
60`stage: "staging"`.
61
62## A branch per stage
63
64Below the database line, nothing is conditional. Every stage —
65including each PR stage — forks its own branch:
66
67```typescript
68const branch = yield* Planetscale.PostgresBranch("app-branch", {
69 database,
70 migrations: "./migrations",
71});
72```
73
74Branches fork from `main` by default and carry their own
75`migrations`: pending `.sql` files are applied to the branch on
76every deploy, so each preview migrates itself independently — see
77[Migrations](/planetscale/data/migrations) for ordering, hashing, and the
78tracking table. To fork data as well as schema, `seedData:
79"last_successful_backup"` restores the last backup into the new
80branch.
81
82## Mint a per-stage credential
83
84The credential is scoped to the branch, so it's per-stage too:
85
86```typescript
87const role = yield* Planetscale.PostgresRole("app-role", {
88 database,
89 branch,
90 inheritedRoles: ["postgres"],
91});
92```
93
94`role.origin` (direct) and `role.pooledOrigin` (PSBouncer) are what
95you hand to the runtime consumer — see
96[Postgres](/planetscale/data/postgres) for the role model and the
97direct-vs-pooled distinction.
98
99## The MySQL variant
100
101The MySQL (Vitess) family follows the same two tiers with its own
102resource trio — `MySQLDatabase.ref`, a non-production `MySQLBranch`,
103and a `MySQLPassword` in place of the Postgres role:
104
105```typescript
106const database = stage.startsWith("pr-")
107 ? yield* Planetscale.MySQLDatabase.ref("app-db", {
108 stage: `staging-${stage}`,
109 })
110 : yield* Planetscale.MySQLDatabase("app-db", {
111 region: { slug: "us-east" },
112 clusterSize: "PS_10",
113 });
114
115const branch = yield* Planetscale.MySQLBranch("app-branch", {
116 database,
117 isProduction: false,
118 migrations: "./migrations",
119});
120
121const password = yield* Planetscale.MySQLPassword("app-password", {
122 database,
123 branch,
124 role: "readwriter",
125});
126```
127
128Non-production branches run on the `PS_DEV` size, and the password's
129`role` is a Vitess access level (`"reader"`, `"writer"`, `"admin"`,
130`"readwriter"`) rather than inherited Postgres roles. Migrations
131behave slightly differently on Vitess — DDL commits implicitly, so
132prefer one schema change per file — see the engine differences in
133[Migrations](/planetscale/data/migrations).
134
135## Deploy the owner first
136
137A ref can only resolve against state that exists, so the owning
138stage deploys before the PR stage that references it:
139
140```sh
141alchemy deploy --stage staging-pr-42 # creates the database
142alchemy deploy --stage pr-42 # references it, forks a branch
143```
144
145Deploying `pr-42` first fails at plan time with
146`InvalidReferenceError` — there's nothing under
147`{ stack, stage: "staging-pr-42", id: "app-db" }` to read yet.
148
149## Tear down
150
151```sh
152alchemy destroy --stage pr-42
153```
154
155Destroy removes the per-stage resources — branch, credential,
156compute — but leaves the referenced database untouched: the PR stage
157doesn't own it, so it can't delete it.
158
159## Wire it into your runtime
160
161The snippets above end at a credential. Connecting it to compute is
162runtime-specific:
163
164- [Shared database across stages](/cloudflare/data/shared-database)
165 — the general `Resource.ref` pattern, step by step.
166- [Branch from a shared database](/cloudflare/data/branch-from-shared-database)
167 — the full walkthrough of this pattern with a Cloudflare Worker on
168 the other end.
169- [Hyperdrive](/cloudflare/data/hyperdrive) — `role.origin` /
170 `password.origin` plug straight into a
171 `Cloudflare.Hyperdrive.Connection`.
172
173Complete working projects:
174[`cloudflare-planetscale-postgres-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle)
175and
176[`cloudflare-planetscale-mysql-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-mysql-drizzle).
177
178## Where next
179
180- [Drizzle ORM with PlanetScale](/planetscale/guides/drizzle) —
181 generate the migrations each preview branch applies.
182- [Migrations](/planetscale/data/migrations) — how migration files
183 are ordered, hashed, and tracked.
184- [Postgres](/planetscale/data/postgres) and [MySQL](/planetscale/data/mysql)
185 — the two resource families used above.