| 1 | --- |
| 2 | title: Single Stack |
| 3 | description: One alchemy.run.ts at the workspace root deploys every app in the monorepo — one plan, one state file, and every path anchored to the file that declares it. |
| 4 | --- |
| 5 | |
| 6 | One `alchemy.run.ts` at the workspace root deploys every app in the monorepo |
| 7 | ([`examples/monorepo-single-stack`](https://github.com/alchemy-run/alchemy/tree/main/examples/monorepo-single-stack)): |
| 8 | |
| 9 | ```sh |
| 10 | . |
| 11 | ├── package.json # workspaces: ["frontend", "backend"] |
| 12 | ├── alchemy.run.ts # the one Stack — deploys both apps |
| 13 | ├── backend/ |
| 14 | │ └── src/ |
| 15 | │ └── Service.ts # the Worker — this file is its own entry |
| 16 | └── frontend/ |
| 17 | ├── index.html # Vite app |
| 18 | └── src/ |
| 19 | └── main.tsx |
| 20 | ``` |
| 21 | |
| 22 | The entire Stack: |
| 23 | |
| 24 | ```typescript |
| 25 | // alchemy.run.ts |
| 26 | import * as Alchemy from "alchemy"; |
| 27 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 28 | import * as Effect from "effect/Effect"; |
| 29 | import { Path } from "effect/Path"; |
| 30 | import Service from "./backend/src/Service.ts"; |
| 31 | |
| 32 | export default Alchemy.Stack( |
| 33 | "Monorepo", |
| 34 | { |
| 35 | providers: Cloudflare.providers(), |
| 36 | state: Cloudflare.state(), |
| 37 | }, |
| 38 | Effect.gen(function* () { |
| 39 | const backend = yield* Service; |
| 40 | const path = yield* Path; |
| 41 | |
| 42 | const website = yield* Cloudflare.Website.Vite("Website", { |
| 43 | rootDir: path.resolve(import.meta.dirname, "frontend"), |
| 44 | env: { |
| 45 | VITE_API_URL: backend.url.as<string>(), |
| 46 | }, |
| 47 | }); |
| 48 | |
| 49 | return { |
| 50 | backendUrl: backend.url.as<string>(), |
| 51 | websiteUrl: website.url.as<string>(), |
| 52 | }; |
| 53 | }), |
| 54 | ); |
| 55 | ``` |
| 56 | |
| 57 | Run `alchemy deploy` (or `alchemy dev`) from the workspace root and both apps |
| 58 | go up in one plan, tracked in one state file. The rest of this page explains |
| 59 | the two lines that make the monorepo shape work: `main: import.meta.url` and |
| 60 | `rootDir`. |
| 61 | |
| 62 | ## Anchor every path |
| 63 | |
| 64 | :::caution |
| 65 | `alchemy deploy` and `alchemy dev` run from the workspace root, so **relative |
| 66 | paths resolve from the root**, not from the file that declares the resource. |
| 67 | A Worker declared in `backend/` with `main: "src/worker.ts"` resolves to |
| 68 | `<root>/src/worker.ts` — a file that doesn't exist. |
| 69 | ::: |
| 70 | |
| 71 | Anchor every path to the file that declares it, using `import.meta`: |
| 72 | |
| 73 | | Declaring | Use | |
| 74 | | ------------------------------------------ | ------------------------------------------------------- | |
| 75 | | a Worker whose defining file is its entry | `main: import.meta.url` | |
| 76 | | a Worker with a separate entry file | `main: new URL("./src/worker.ts", import.meta.url).href` | |
| 77 | | a Vite website in a subdirectory | `rootDir: path.resolve(import.meta.dirname, "frontend")` | |
| 78 | | a custom Worker entry inside a Vite app | `vite: { main: "src/worker.ts" }` — resolves from `rootDir` | |
| 79 | |
| 80 | `import.meta.url` and `import.meta.dirname` always point at the file being |
| 81 | evaluated, so the resource resolves the same way no matter which directory |
| 82 | `alchemy` runs from. |
| 83 | |
| 84 | ## The backend Worker |
| 85 | |
| 86 | Each app declares its own Worker in its own package: |
| 87 | |
| 88 | ```typescript |
| 89 | // backend/src/Service.ts |
| 90 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 91 | import * as Effect from "effect/Effect"; |
| 92 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 93 | |
| 94 | export default class Service extends Cloudflare.Worker<Service>()( |
| 95 | "Service", |
| 96 | { main: import.meta.url }, |
| 97 | Effect.gen(function* () { |
| 98 | return { |
| 99 | fetch: Effect.succeed(HttpServerResponse.text("Hello from the backend")), |
| 100 | }; |
| 101 | }), |
| 102 | ) {} |
| 103 | ``` |
| 104 | |
| 105 | `main: import.meta.url` makes this file its own entry — a file URL, absolute |
| 106 | by construction, so the root `alchemy.run.ts` can import `Service` from |
| 107 | anywhere and bundling still finds the right module. |
| 108 | |
| 109 | The example serves a typed `HttpApi` instead of a plain `fetch` handler and |
| 110 | shares its typed client with the frontend — see |
| 111 | [Effect HTTP API](/cloudflare/apis/effect-http-api) for that pattern. |
| 112 | |
| 113 | ## The Vite website |
| 114 | |
| 115 | ```typescript |
| 116 | const website = yield* Cloudflare.Website.Vite("Website", { |
| 117 | rootDir: path.resolve(import.meta.dirname, "frontend"), |
| 118 | }); |
| 119 | ``` |
| 120 | |
| 121 | `rootDir` is Vite's `root` — where `index.html`, `vite.config.ts`, and the |
| 122 | app source live. It defaults to `process.cwd()`, which in a monorepo is the |
| 123 | workspace root, so point it at the app's directory explicitly. Anchoring with |
| 124 | `import.meta.dirname` keeps it correct regardless of where `alchemy` runs. |
| 125 | |
| 126 | ## Wire the Worker's URL into the frontend |
| 127 | |
| 128 | ```typescript |
| 129 | const website = yield* Cloudflare.Website.Vite("Website", { |
| 130 | rootDir: path.resolve(import.meta.dirname, "frontend"), |
| 131 | env: { |
| 132 | VITE_API_URL: backend.url.as<string>(), |
| 133 | }, |
| 134 | }); |
| 135 | ``` |
| 136 | |
| 137 | `backend.url` is an `Output<string>` — passing it into `env` makes Alchemy |
| 138 | deploy the Worker first, then build the website with the resolved URL baked |
| 139 | in. The frontend reads it like any other Vite env var: |
| 140 | |
| 141 | ```typescript |
| 142 | // frontend/src/main.tsx |
| 143 | const API_URL = import.meta.env.VITE_API_URL; |
| 144 | ``` |
| 145 | |
| 146 | This is the payoff of the single-stack shape: the frontend consumes the |
| 147 | backend's URL directly off the in-memory Output — no cross-stack references, |
| 148 | no deploy ordering to manage. |
| 149 | |
| 150 | ## Custom Worker entry inside the Vite app |
| 151 | |
| 152 | By default `Website.Vite` deploys the server entry your framework produces. |
| 153 | If the app ships its own Worker module (extra handlers, Durable Objects), |
| 154 | point `vite.main` at it: |
| 155 | |
| 156 | ```typescript |
| 157 | const website = yield* Cloudflare.Website.Vite("Website", { |
| 158 | rootDir: path.resolve(import.meta.dirname, "frontend"), |
| 159 | vite: { |
| 160 | main: "src/worker.ts", |
| 161 | }, |
| 162 | }); |
| 163 | ``` |
| 164 | |
| 165 | `vite.main` is the one relative path that does *not* resolve from the |
| 166 | workspace root — it resolves from `rootDir`, so `src/worker.ts` means |
| 167 | `frontend/src/worker.ts`. |
| 168 | |
| 169 | ## Add more apps |
| 170 | |
| 171 | The pattern scales by repetition: declare each app's Worker in its own |
| 172 | package with an anchored `main`, import them all into the one Stack, and |
| 173 | `yield*` each one. A Worker whose entry is a separate file anchors it with |
| 174 | `new URL`: |
| 175 | |
| 176 | ```typescript |
| 177 | // apps/api/Api.ts |
| 178 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 179 | |
| 180 | export const Api = Cloudflare.Worker("Api", { |
| 181 | main: new URL("./src/worker.ts", import.meta.url).href, |
| 182 | }); |
| 183 | ``` |
| 184 | |
| 185 | ```typescript |
| 186 | // alchemy.run.ts |
| 187 | const api = yield* Api; |
| 188 | ``` |
| 189 | |
| 190 | ## Deploy |
| 191 | |
| 192 | ```sh |
| 193 | alchemy deploy |
| 194 | ``` |
| 195 | |
| 196 | One plan, one apply, one state file. Both apps go up together; both come |
| 197 | down together with `alchemy destroy`. |
| 198 | |
| 199 | ## Where next |
| 200 | |
| 201 | - [Monorepo](/project-structure/monorepo) — the high-level chooser. |
| 202 | - [Multiple Stacks](/project-structure/monorepo-multi-stack) — split the |
| 203 | deploy per package when cadences diverge. |
| 204 | - [File layout](/project-structure/file-layout) — the single-package layout |
| 205 | this builds on. |
| 206 | - [Effect HTTP API](/cloudflare/apis/effect-http-api) — share a typed |
| 207 | `HttpApi` schema and client between the Worker and the frontend. |