1---
2title: Branch from a shared database
3description: 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
6import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
7
8In the [Hyperdrive](/cloudflare/data/hyperdrive)
9tutorial you provisioned a `Neon.Project` and a `Neon.Branch`, then
10[Drizzle](/cloudflare/data/drizzle) layered automatic
11migrations on top. Both work fine with a single stage — but the
12moment you start deploying PR previews (`pr-147`, `pr-148`, …) the
13default 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
21The fix is to share the **project** across stages — long-lived,
22owned by `staging` — and only fork a per-stage **branch** off it.
23PR stages get a fast, isolated, copy-on-write database without
24touching project creation.
25
26## Where we left off
27
28Recap of `src/Db.ts` from the Drizzle tutorial — every stage
29unconditionally creates its own `Neon.Project`:
30
31```typescript
32// src/Db.ts
33import * as Cloudflare from "alchemy/Cloudflare";
34import * as Drizzle from "alchemy/Drizzle";
35import * as Neon from "alchemy/Neon";
36import * as Effect from "effect/Effect";
37
38export 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
56export 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
64Today we'll change the project line — and only the project line —
65so PR stages reuse `staging`'s.
66
67## Read the active stage
68
69The branching decision needs to know which stage is being
70deployed. Pull it off the `Stack` service, which Alchemy provides
71to 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
92reference one.
93
94## Reference the shared project for PR stages
95
96Replace the unconditional `Neon.Project(...)` with a conditional
97that 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
111already 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.
121The downstream `Neon.Branch` doesn't know (or care) which path
122produced it.
123
124For the bigger picture see [Shared database across stages](/cloudflare/data/shared-database).
125
126## The branch is still per-stage
127
128Nothing changes below the project line. `Neon.Branch` still creates
129a fresh branch per stage, off whichever project the conditional
130returned:
131
132```typescript
133const branch = yield* Neon.Branch("app-branch", {
134 project,
135 migrations: schema,
136});
137```
138
139Branches are copy-on-write and free, so each PR stage gets a fully
140isolated database — its own writes, its own migration history —
141without paying for a whole Postgres cluster.
142
143## Assembled file
144
145```typescript
146// src/Db.ts
147import * as Alchemy from "alchemy";
148import * as Cloudflare from "alchemy/Cloudflare";
149import * as Drizzle from "alchemy/Drizzle";
150import * as Neon from "alchemy/Neon";
151import * as Effect from "effect/Effect";
152
153export 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
175export 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
185PR stages can only reference what `staging` has already deployed,
186so `staging` has to go first:
187
188```sh
189bun alchemy deploy --stage staging
190```
191
192This runs the full path — creates `app-db` (the project),
193`app-branch` against it, and `app-hyperdrive` in front. The state
194for `Neon.Project("app-db")` lands in the `staging` state file,
195which is what PR stages will read against.
196
197## Deploy a PR stage
198
199Now deploy a PR-preview stage. The same code, different stage flag:
200
201```sh
202bun alchemy deploy --stage pr-147
203```
204
205The 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
215The PR stage has its own isolated branch, its own Hyperdrive, its
216own Worker — and it shares the project with `staging`.
217
218:::tip
219If you `alchemy deploy --stage pr-147` *before* `staging` exists,
220plan fails with `InvalidReferenceError` because there's nothing
221under `{ stack, stage: "staging", id: "app-db" }` to read. Deploy
222`staging` first.
223:::
224
225## Tear it down
226
227```sh
228bun alchemy destroy --stage pr-147
229```
230
231Destroy removes the per-stage resources — the branch, the
232Hyperdrive, the Worker — but leaves the referenced project
233untouched. Alchemy doesn't own `app-db` from this stage's
234perspective, so it can't delete it. The shared project keeps
235serving every other PR stage and `staging` itself.
236
237## The same recipe on PlanetScale
238
239Nothing here is Neon-specific. [PlanetScale](/planetscale) has the
240same shape — an expensive long-lived database, cheap per-stage
241branches — so the same conditional works, with
242`Planetscale.PostgresDatabase.ref` (or `MySQLDatabase.ref`) in
243place of `Neon.Project.ref`:
244
245<Tabs syncKey="ps-engine">
246 <TabItem label="Postgres">
247
248```typescript
249// src/Db.ts
250export 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
287export 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
318The 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
324Look closely at the ref: it targets `staging-${stage}`, not a
325single global `staging`. Deploying `pr-42` reads the database from
326a parallel `staging-pr-42` stage:
327
328```typescript
329Planetscale.PostgresDatabase.ref("app-db", {
330 stage: `staging-${stage}`, // pr-42 → staging-pr-42
331});
332```
333
334Deriving the staging stage name from the PR stage gives every PR
335its own database owner — isolated data per PR — while the
336create-vs-reference split stays identical. Deploy `staging-pr-42`
337first (it takes the create path), then `pr-42` (it takes the ref
338path). 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
343PlanetScale's connection string comes from the per-stage
344credential rather than the branch, so the Hyperdrive origin
345changes accordingly:
346
347```typescript
348export 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
357The full working projects live at
358[`cloudflare-planetscale-postgres-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle)
359and
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
364You 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
372The same `Resource.ref` pattern works for any other expensive,
373shared resource you have — an S3 bucket of seed fixtures, a global
374rate-limiter Durable Object, a DNS zone. Anywhere a per-stage
375copy is wasteful, lift it to a long-lived stage and reference it
376from 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.