| 1 | --- |
| 2 | title: Branch from a shared database |
| 3 | description: Have ephemeral PR-preview stages reference a long-lived Neon or PlanetScale database from a staging stage instead of provisioning their own — fast previews, cheap branches, no extra clusters. |
| 4 | --- |
| 5 | |
| 6 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 7 | |
| 8 | In the [Hyperdrive](/cloudflare/data/hyperdrive) |
| 9 | tutorial you provisioned a `Neon.Project` and a `Neon.Branch`, then |
| 10 | [Drizzle](/cloudflare/data/drizzle) layered automatic |
| 11 | migrations on top. Both work fine with a single stage — but the |
| 12 | moment you start deploying PR previews (`pr-147`, `pr-148`, …) the |
| 13 | default flow becomes painful: |
| 14 | |
| 15 | - Every PR stage provisions its own `Neon.Project` — a whole |
| 16 | Postgres cluster, with cold starts, project-count limits, and a |
| 17 | full migration replay against an empty branch. |
| 18 | - Tearing the PR stage down deletes the project too, throwing away |
| 19 | the data and any seeds you'd attached. |
| 20 | |
| 21 | The fix is to share the **project** across stages — long-lived, |
| 22 | owned by `staging` — and only fork a per-stage **branch** off it. |
| 23 | PR stages get a fast, isolated, copy-on-write database without |
| 24 | touching project creation. |
| 25 | |
| 26 | ## Where we left off |
| 27 | |
| 28 | Recap of `src/Db.ts` from the Drizzle tutorial — every stage |
| 29 | unconditionally creates its own `Neon.Project`: |
| 30 | |
| 31 | ```typescript |
| 32 | // src/Db.ts |
| 33 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 34 | import * as Drizzle from "alchemy/Drizzle"; |
| 35 | import * as Neon from "alchemy/Neon"; |
| 36 | import * as Effect from "effect/Effect"; |
| 37 | |
| 38 | export const NeonDb = Effect.gen(function* () { |
| 39 | const schema = yield* Drizzle.Schema("app-schema", { |
| 40 | schema: "./src/schema.ts", |
| 41 | out: "./migrations", |
| 42 | }); |
| 43 | |
| 44 | const project = yield* Neon.Project("app-db", { |
| 45 | region: "aws-us-east-1", |
| 46 | }); |
| 47 | |
| 48 | const branch = yield* Neon.Branch("app-branch", { |
| 49 | project, |
| 50 | migrations: schema, |
| 51 | }); |
| 52 | |
| 53 | return { project, branch, schema }; |
| 54 | }); |
| 55 | |
| 56 | export const Hyperdrive = Effect.gen(function* () { |
| 57 | const { branch } = yield* NeonDb; |
| 58 | return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { |
| 59 | origin: branch.origin, |
| 60 | }); |
| 61 | }); |
| 62 | ``` |
| 63 | |
| 64 | Today we'll change the project line — and only the project line — |
| 65 | so PR stages reuse `staging`'s. |
| 66 | |
| 67 | ## Read the active stage |
| 68 | |
| 69 | The branching decision needs to know which stage is being |
| 70 | deployed. Pull it off the `Stack` service, which Alchemy provides |
| 71 | to every stack effect: |
| 72 | |
| 73 | ```diff lang="typescript" |
| 74 | +import * as Alchemy from "alchemy"; |
| 75 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 76 | import * as Drizzle from "alchemy/Drizzle"; |
| 77 | import * as Neon from "alchemy/Neon"; |
| 78 | import * as Effect from "effect/Effect"; |
| 79 | |
| 80 | export const NeonDb = Effect.gen(function* () { |
| 81 | + const { stage } = yield* Alchemy.Stack; |
| 82 | + |
| 83 | const schema = yield* Drizzle.Schema("app-schema", { |
| 84 | schema: "./src/schema.ts", |
| 85 | out: "./migrations", |
| 86 | }); |
| 87 | ``` |
| 88 | |
| 89 | `stage` is whatever was passed to `bun alchemy deploy --stage <name>` |
| 90 | — `dev_sam`, `staging`, `prod`, or `pr-147`. We'll branch on |
| 91 | `stage.startsWith("pr-")` to decide whether to create a project or |
| 92 | reference one. |
| 93 | |
| 94 | ## Reference the shared project for PR stages |
| 95 | |
| 96 | Replace the unconditional `Neon.Project(...)` with a conditional |
| 97 | that uses `Neon.Project.ref(...)` when the stage is a PR preview: |
| 98 | |
| 99 | ```diff lang="typescript" |
| 100 | - const project = yield* Neon.Project("app-db", { |
| 101 | - region: "aws-us-east-1", |
| 102 | - }); |
| 103 | + const project = stage.startsWith("pr-") |
| 104 | + ? yield* Neon.Project.ref("app-db", { stage: "staging" }) |
| 105 | + : yield* Neon.Project("app-db", { |
| 106 | + region: "aws-us-east-1", |
| 107 | + }); |
| 108 | ``` |
| 109 | |
| 110 | `Resource.ref` returns a typed reference to a resource that's |
| 111 | already been deployed in another stage. The lookup is keyed by |
| 112 | `{ stack, stage, id }`: |
| 113 | |
| 114 | - The logical ID `"app-db"` matches the project the `staging` |
| 115 | stage creates. |
| 116 | - `stage: "staging"` tells the lookup which stage's state file to |
| 117 | read from. |
| 118 | - The current stack is implied — both stages share it. |
| 119 | |
| 120 | `project` has type `Neon.Project` regardless of which branch ran. |
| 121 | The downstream `Neon.Branch` doesn't know (or care) which path |
| 122 | produced it. |
| 123 | |
| 124 | For the bigger picture see [Shared database across stages](/cloudflare/data/shared-database). |
| 125 | |
| 126 | ## The branch is still per-stage |
| 127 | |
| 128 | Nothing changes below the project line. `Neon.Branch` still creates |
| 129 | a fresh branch per stage, off whichever project the conditional |
| 130 | returned: |
| 131 | |
| 132 | ```typescript |
| 133 | const branch = yield* Neon.Branch("app-branch", { |
| 134 | project, |
| 135 | migrations: schema, |
| 136 | }); |
| 137 | ``` |
| 138 | |
| 139 | Branches are copy-on-write and free, so each PR stage gets a fully |
| 140 | isolated database — its own writes, its own migration history — |
| 141 | without paying for a whole Postgres cluster. |
| 142 | |
| 143 | ## Assembled file |
| 144 | |
| 145 | ```typescript |
| 146 | // src/Db.ts |
| 147 | import * as Alchemy from "alchemy"; |
| 148 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 149 | import * as Drizzle from "alchemy/Drizzle"; |
| 150 | import * as Neon from "alchemy/Neon"; |
| 151 | import * as Effect from "effect/Effect"; |
| 152 | |
| 153 | export const NeonDb = Effect.gen(function* () { |
| 154 | const { stage } = yield* Alchemy.Stack; |
| 155 | |
| 156 | const schema = yield* Drizzle.Schema("app-schema", { |
| 157 | schema: "./src/schema.ts", |
| 158 | out: "./migrations", |
| 159 | }); |
| 160 | |
| 161 | const project = stage.startsWith("pr-") |
| 162 | ? yield* Neon.Project.ref("app-db", { stage: "staging" }) |
| 163 | : yield* Neon.Project("app-db", { |
| 164 | region: "aws-us-east-1", |
| 165 | }); |
| 166 | |
| 167 | const branch = yield* Neon.Branch("app-branch", { |
| 168 | project, |
| 169 | migrations: schema, |
| 170 | }); |
| 171 | |
| 172 | return { project, branch, schema }; |
| 173 | }); |
| 174 | |
| 175 | export const Hyperdrive = Effect.gen(function* () { |
| 176 | const { branch } = yield* NeonDb; |
| 177 | return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { |
| 178 | origin: branch.origin, |
| 179 | }); |
| 180 | }); |
| 181 | ``` |
| 182 | |
| 183 | ## Deploy `staging` to seed the shared project |
| 184 | |
| 185 | PR stages can only reference what `staging` has already deployed, |
| 186 | so `staging` has to go first: |
| 187 | |
| 188 | ```sh |
| 189 | bun alchemy deploy --stage staging |
| 190 | ``` |
| 191 | |
| 192 | This runs the full path — creates `app-db` (the project), |
| 193 | `app-branch` against it, and `app-hyperdrive` in front. The state |
| 194 | for `Neon.Project("app-db")` lands in the `staging` state file, |
| 195 | which is what PR stages will read against. |
| 196 | |
| 197 | ## Deploy a PR stage |
| 198 | |
| 199 | Now deploy a PR-preview stage. The same code, different stage flag: |
| 200 | |
| 201 | ```sh |
| 202 | bun alchemy deploy --stage pr-147 |
| 203 | ``` |
| 204 | |
| 205 | The plan is noticeably shorter: |
| 206 | |
| 207 | - `Neon.Project("app-db")` → resolved as a **reference** to |
| 208 | staging's project. No API call. |
| 209 | - `Neon.Branch("app-branch")` → freshly created against the |
| 210 | referenced project. |
| 211 | - `Cloudflare.Hyperdrive.Connection("app-hyperdrive")` → points at the new |
| 212 | branch's `origin`. |
| 213 | - `Api` → freshly created. |
| 214 | |
| 215 | The PR stage has its own isolated branch, its own Hyperdrive, its |
| 216 | own Worker — and it shares the project with `staging`. |
| 217 | |
| 218 | :::tip |
| 219 | If you `alchemy deploy --stage pr-147` *before* `staging` exists, |
| 220 | plan fails with `InvalidReferenceError` because there's nothing |
| 221 | under `{ stack, stage: "staging", id: "app-db" }` to read. Deploy |
| 222 | `staging` first. |
| 223 | ::: |
| 224 | |
| 225 | ## Tear it down |
| 226 | |
| 227 | ```sh |
| 228 | bun alchemy destroy --stage pr-147 |
| 229 | ``` |
| 230 | |
| 231 | Destroy removes the per-stage resources — the branch, the |
| 232 | Hyperdrive, the Worker — but leaves the referenced project |
| 233 | untouched. Alchemy doesn't own `app-db` from this stage's |
| 234 | perspective, so it can't delete it. The shared project keeps |
| 235 | serving every other PR stage and `staging` itself. |
| 236 | |
| 237 | ## The same recipe on PlanetScale |
| 238 | |
| 239 | Nothing here is Neon-specific. [PlanetScale](/planetscale) has the |
| 240 | same shape — an expensive long-lived database, cheap per-stage |
| 241 | branches — so the same conditional works, with |
| 242 | `Planetscale.PostgresDatabase.ref` (or `MySQLDatabase.ref`) in |
| 243 | place of `Neon.Project.ref`: |
| 244 | |
| 245 | <Tabs syncKey="ps-engine"> |
| 246 | <TabItem label="Postgres"> |
| 247 | |
| 248 | ```typescript |
| 249 | // src/Db.ts |
| 250 | export const PlanetscaleDb = Effect.gen(function* () { |
| 251 | const { stage } = yield* Alchemy.Stack; |
| 252 | |
| 253 | const schema = yield* Drizzle.Schema("app-schema", { |
| 254 | schema: "./src/schema.ts", |
| 255 | out: "./migrations", |
| 256 | }); |
| 257 | |
| 258 | const database = stage.startsWith("pr-") |
| 259 | ? yield* Planetscale.PostgresDatabase.ref("app-db", { |
| 260 | stage: `staging-${stage}`, |
| 261 | }) |
| 262 | : yield* Planetscale.PostgresDatabase("app-db", { |
| 263 | region: { slug: "us-east" }, |
| 264 | clusterSize: "PS_10", |
| 265 | }); |
| 266 | |
| 267 | const branch = yield* Planetscale.PostgresBranch("app-branch", { |
| 268 | database, |
| 269 | migrations: schema, |
| 270 | }); |
| 271 | |
| 272 | const role = yield* Planetscale.PostgresRole("app-role", { |
| 273 | database, |
| 274 | branch, |
| 275 | inheritedRoles: ["postgres"], |
| 276 | }); |
| 277 | |
| 278 | return { database, branch, role, schema }; |
| 279 | }); |
| 280 | ``` |
| 281 | |
| 282 | </TabItem> |
| 283 | <TabItem label="MySQL"> |
| 284 | |
| 285 | ```typescript |
| 286 | // src/Db.ts |
| 287 | export const PlanetscaleDb = Effect.gen(function* () { |
| 288 | const { stage } = yield* Alchemy.Stack; |
| 289 | |
| 290 | const database = stage.startsWith("pr-") |
| 291 | ? yield* Planetscale.MySQLDatabase.ref("app-db", { |
| 292 | stage: `staging-${stage}`, |
| 293 | }) |
| 294 | : yield* Planetscale.MySQLDatabase("app-db", { |
| 295 | region: { slug: "us-east" }, |
| 296 | clusterSize: "PS_10", |
| 297 | }); |
| 298 | |
| 299 | const branch = yield* Planetscale.MySQLBranch("app-branch", { |
| 300 | database, |
| 301 | isProduction: false, |
| 302 | migrations: "./migrations", |
| 303 | }); |
| 304 | |
| 305 | const password = yield* Planetscale.MySQLPassword("app-password", { |
| 306 | database, |
| 307 | branch, |
| 308 | role: "readwriter", |
| 309 | }); |
| 310 | |
| 311 | return { database, branch, password }; |
| 312 | }); |
| 313 | ``` |
| 314 | |
| 315 | </TabItem> |
| 316 | </Tabs> |
| 317 | |
| 318 | The database is the shared tier; the branch and its credential |
| 319 | (`PostgresRole` / `MySQLPassword`) are per-stage, just like |
| 320 | `Neon.Branch` was. |
| 321 | |
| 322 | ## A staging tier per PR |
| 323 | |
| 324 | Look closely at the ref: it targets `staging-${stage}`, not a |
| 325 | single global `staging`. Deploying `pr-42` reads the database from |
| 326 | a parallel `staging-pr-42` stage: |
| 327 | |
| 328 | ```typescript |
| 329 | Planetscale.PostgresDatabase.ref("app-db", { |
| 330 | stage: `staging-${stage}`, // pr-42 → staging-pr-42 |
| 331 | }); |
| 332 | ``` |
| 333 | |
| 334 | Deriving the staging stage name from the PR stage gives every PR |
| 335 | its own database owner — isolated data per PR — while the |
| 336 | create-vs-reference split stays identical. Deploy `staging-pr-42` |
| 337 | first (it takes the create path), then `pr-42` (it takes the ref |
| 338 | path). Prefer one database shared by all PRs? Use |
| 339 | `stage: "staging"` and you're back to exactly the Neon flow above. |
| 340 | |
| 341 | ## Point Hyperdrive at the credential |
| 342 | |
| 343 | PlanetScale's connection string comes from the per-stage |
| 344 | credential rather than the branch, so the Hyperdrive origin |
| 345 | changes accordingly: |
| 346 | |
| 347 | ```typescript |
| 348 | export const Hyperdrive = Effect.gen(function* () { |
| 349 | const { role } = yield* PlanetscaleDb; // { password } for MySQL |
| 350 | return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { |
| 351 | origin: role.origin, // password.origin for MySQL |
| 352 | caching: { disabled: true }, |
| 353 | }); |
| 354 | }); |
| 355 | ``` |
| 356 | |
| 357 | The full working projects live at |
| 358 | [`cloudflare-planetscale-postgres-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle) |
| 359 | and |
| 360 | [`cloudflare-planetscale-mysql-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-mysql-drizzle). |
| 361 | |
| 362 | ## Where to from here |
| 363 | |
| 364 | You now have: |
| 365 | |
| 366 | - A **shared** Neon project owned by `staging`, surviving every |
| 367 | PR stage's lifecycle. |
| 368 | - **Per-stage** Neon branches with their own migrations and data. |
| 369 | - A deploy/destroy story that scales to dozens of preview |
| 370 | environments without flooding Neon with one project per PR. |
| 371 | |
| 372 | The same `Resource.ref` pattern works for any other expensive, |
| 373 | shared resource you have — an S3 bucket of seed fixtures, a global |
| 374 | rate-limiter Durable Object, a DNS zone. Anywhere a per-stage |
| 375 | copy is wasteful, lift it to a long-lived stage and reference it |
| 376 | from the rest. |
| 377 | |
| 378 | - [Neon](/neon) — projects, branches, and migrations. |
| 379 | - [PlanetScale](/planetscale) — the Postgres and MySQL resource |
| 380 | families used above. |
| 381 | - [Shared database across stages](/cloudflare/data/shared-database) |
| 382 | — the general `Resource.ref` pattern, including cross-stack refs. |