| 1 | --- |
| 2 | title: Preview branches per PR |
| 3 | description: 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 | |
| 6 | PlanetScale's branch model maps directly onto alchemy stages: the |
| 7 | database is the expensive, long-lived tier; branches are cheap forks. |
| 8 | Split your stages the same way and every PR gets an isolated schema |
| 9 | and 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 | |
| 15 | The pattern is identical for both engines; only the resource names |
| 16 | and the credential model differ. New here? [Set up |
| 17 | credentials](/planetscale/setup) first. |
| 18 | |
| 19 | ## Read the active stage |
| 20 | |
| 21 | The create-vs-reference decision keys off the stage being deployed, |
| 22 | which the `Stack` service exposes: |
| 23 | |
| 24 | ```typescript |
| 25 | import * as Alchemy from "alchemy"; |
| 26 | |
| 27 | const { 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 | |
| 35 | PR stages reference the database with `PostgresDatabase.ref`; every |
| 36 | other stage creates it: |
| 37 | |
| 38 | ```typescript |
| 39 | import * as Planetscale from "alchemy/Planetscale"; |
| 40 | |
| 41 | const 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 |
| 52 | attributes from another stage's state at plan time — the lookup is |
| 53 | keyed by `{ stack, stage, id }`, so the logical ID `"app-db"` must |
| 54 | match what the owning stage created. `database` is typed |
| 55 | `Planetscale.PostgresDatabase` on both paths, and downstream |
| 56 | resources don't care which one ran. Note the ref targets |
| 57 | `staging-${stage}`, not a global `"staging"`: `pr-42` reads from a |
| 58 | parallel `staging-pr-42` stage, so each PR's data lives in its own |
| 59 | database. Prefer one database shared by every PR? Use |
| 60 | `stage: "staging"`. |
| 61 | |
| 62 | ## A branch per stage |
| 63 | |
| 64 | Below the database line, nothing is conditional. Every stage — |
| 65 | including each PR stage — forks its own branch: |
| 66 | |
| 67 | ```typescript |
| 68 | const branch = yield* Planetscale.PostgresBranch("app-branch", { |
| 69 | database, |
| 70 | migrations: "./migrations", |
| 71 | }); |
| 72 | ``` |
| 73 | |
| 74 | Branches fork from `main` by default and carry their own |
| 75 | `migrations`: pending `.sql` files are applied to the branch on |
| 76 | every deploy, so each preview migrates itself independently — see |
| 77 | [Migrations](/planetscale/data/migrations) for ordering, hashing, and the |
| 78 | tracking table. To fork data as well as schema, `seedData: |
| 79 | "last_successful_backup"` restores the last backup into the new |
| 80 | branch. |
| 81 | |
| 82 | ## Mint a per-stage credential |
| 83 | |
| 84 | The credential is scoped to the branch, so it's per-stage too: |
| 85 | |
| 86 | ```typescript |
| 87 | const 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 |
| 95 | you hand to the runtime consumer — see |
| 96 | [Postgres](/planetscale/data/postgres) for the role model and the |
| 97 | direct-vs-pooled distinction. |
| 98 | |
| 99 | ## The MySQL variant |
| 100 | |
| 101 | The MySQL (Vitess) family follows the same two tiers with its own |
| 102 | resource trio — `MySQLDatabase.ref`, a non-production `MySQLBranch`, |
| 103 | and a `MySQLPassword` in place of the Postgres role: |
| 104 | |
| 105 | ```typescript |
| 106 | const 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 | |
| 115 | const branch = yield* Planetscale.MySQLBranch("app-branch", { |
| 116 | database, |
| 117 | isProduction: false, |
| 118 | migrations: "./migrations", |
| 119 | }); |
| 120 | |
| 121 | const password = yield* Planetscale.MySQLPassword("app-password", { |
| 122 | database, |
| 123 | branch, |
| 124 | role: "readwriter", |
| 125 | }); |
| 126 | ``` |
| 127 | |
| 128 | Non-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 |
| 131 | behave slightly differently on Vitess — DDL commits implicitly, so |
| 132 | prefer one schema change per file — see the engine differences in |
| 133 | [Migrations](/planetscale/data/migrations). |
| 134 | |
| 135 | ## Deploy the owner first |
| 136 | |
| 137 | A ref can only resolve against state that exists, so the owning |
| 138 | stage deploys before the PR stage that references it: |
| 139 | |
| 140 | ```sh |
| 141 | alchemy deploy --stage staging-pr-42 # creates the database |
| 142 | alchemy deploy --stage pr-42 # references it, forks a branch |
| 143 | ``` |
| 144 | |
| 145 | Deploying `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 |
| 152 | alchemy destroy --stage pr-42 |
| 153 | ``` |
| 154 | |
| 155 | Destroy removes the per-stage resources — branch, credential, |
| 156 | compute — but leaves the referenced database untouched: the PR stage |
| 157 | doesn't own it, so it can't delete it. |
| 158 | |
| 159 | ## Wire it into your runtime |
| 160 | |
| 161 | The snippets above end at a credential. Connecting it to compute is |
| 162 | runtime-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 | |
| 173 | Complete working projects: |
| 174 | [`cloudflare-planetscale-postgres-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle) |
| 175 | and |
| 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. |