1---
2title: Multiple Stacks
3description: Each package owns its own alchemy.run.ts. The frontend consumes the backend's deployed outputs through a typed Stack handle, resolved from the state store at plan time, so each package deploys and destroys independently.
4---
5
6Each package owns its own `alchemy.run.ts`
7([`examples/monorepo-multi-stack`](https://github.com/alchemy-run/alchemy/tree/main/examples/monorepo-multi-stack)):
8
9```sh
10.
11├── package.json # workspaces: ["frontend", "backend"]
12├── backend/
13│ ├── alchemy.run.ts # Backend Stack — deploys the Worker
14│ ├── package.json # name: "backend"
15│ └── src/
16│ ├── Service.ts # the Worker
17│ └── Stack.ts # typed handle — outputs { url: string }
18└── frontend/
19 ├── alchemy.run.ts # Frontend Stack — references Backend
20 ├── package.json # depends on "backend": "workspace:*"
21 └── src/
22 └── main.tsx # reads import.meta.env.VITE_API_URL
23```
24
25There is no shared plan and no live connection between the Stacks. The
26backend deploys and persists its outputs to the state store; the frontend's
27plan reads those persisted outputs back through a **typed Stack handle**.
28That one indirection is what buys independent deploys, independent destroys,
29and independent CI — at the cost of a deploy order.
30
31Three small files make it work.
32
33## The typed Stack handle
34
35```typescript
36// backend/src/Stack.ts
37import * as Alchemy from "alchemy";
38
39export class Backend extends Alchemy.Stack<
40 Backend,
41 {
42 url: string;
43 }
44>()("Backend") {}
45```
46
47The handle is the contract between the two Stacks: it names the Stack
48(`"Backend"`) and declares its output shape (`{ url: string }`). Both sides
49import this class — the backend to deploy against it, the frontend to
50reference it — so TypeScript enforces the contract in both directions.
51
52## The backend Stack
53
54```typescript
55// backend/alchemy.run.ts
56import * as Cloudflare from "alchemy/Cloudflare";
57import * as Effect from "effect/Effect";
58import Service from "./src/Service.ts";
59import { Backend } from "./src/Stack.ts";
60
61export default Backend.make(
62 {
63 providers: Cloudflare.providers(),
64 state: Cloudflare.state(),
65 },
66 Effect.gen(function* () {
67 const api = yield* Service;
68 return {
69 url: api.url.as<string>(),
70 };
71 }),
72);
73```
74
75`Backend.make` is `Alchemy.Stack` bound to the handle: if the returned
76object doesn't match `{ url: string }`, this file fails to typecheck. On
77deploy, the resolved `url` is persisted to the state store as the Stack's
78output.
79
80## The frontend Stack
81
82```typescript
83// frontend/alchemy.run.ts
84import * as Alchemy from "alchemy";
85import * as Cloudflare from "alchemy/Cloudflare";
86import { Backend } from "backend";
87import * as Effect from "effect/Effect";
88
89export default Alchemy.Stack(
90 "Frontend",
91 {
92 providers: Cloudflare.providers(),
93 state: Cloudflare.state(),
94 },
95 Effect.gen(function* () {
96 const backend = yield* Backend;
97
98 const website = yield* Cloudflare.Website.Vite("Website", {
99 env: {
100 VITE_API_URL: backend.url,
101 },
102 });
103
104 return {
105 url: website.url.as<string>(),
106 };
107 }),
108);
109```
110
111`yield* Backend` reads the backend's persisted outputs from the state store
112at plan time and returns them fully typed — `backend.url` is the `string`
113the handle declared. It resolves the **same stage** the frontend is being
114deployed to: the `sam` frontend reads the `sam` backend, `pr-42` reads
115`pr-42`.
116
117The import works because `frontend/package.json` depends on
118`"backend": "workspace:*"` — the handle travels through the workspace like
119any other export.
120
121## Deploy in order
122
123```sh
124cd backend && alchemy deploy --stage sam
125cd frontend && alchemy deploy --stage sam
126```
127
128The backend must be deployed to the stage first — if it isn't, the
129frontend's plan fails with `InvalidReferenceError` because there are no
130persisted outputs to read. Destroy in reverse: frontend first, then backend.
131
132Each `alchemy.run.ts` runs from its own package directory, so relative paths
133(`main`, `rootDir`) resolve against the package — the
134[root-relative path trap](/project-structure/monorepo-single-stack#anchor-every-path)
135of the single-stack layout doesn't apply here.
136
137## Pin to a specific stage
138
139The bare `yield* Backend` (same stage on both sides) is the right default.
140To break stage symmetry — for example, every frontend preview points at the
141production backend — pin with `Backend.stage.<name>`:
142
143```diff lang="typescript"
144 Effect.gen(function* () {
145- const backend = yield* Backend;
146+ const backend = yield* Backend.stage.prod;
147 // ...
148 })
149```
150
151`Backend.stage` is a proxy keyed by stage name — any string works
152(`Backend.stage.staging`, `Backend.stage["pr-42"]`).
153
154## Where next
155
156- [Monorepo](/project-structure/monorepo) — the high-level chooser.
157- [Single Stack](/project-structure/monorepo-single-stack) — the one-Stack
158 alternative: one plan, one state file, paths anchored to the root.
159- [References](/infrastructure-as-code/references) — when to split Stacks,
160 how a reference resolves (typing, plan-time state reads,
161 `InvalidReferenceError`), and stage pinning in depth.