| 1 | --- |
| 2 | title: Waku |
| 3 | description: Deploy a Waku app to Cloudflare Workers with Cloudflare.Website.Waku — RSC server in the Worker, SSG pages as static assets, and a custom-entry seam for Durable Objects. |
| 4 | sidebar: |
| 5 | order: 12 |
| 6 | --- |
| 7 | |
| 8 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 9 | |
| 10 | export const devDeps = "@alchemy.run/frontend-frameworks"; |
| 11 | |
| 12 | `Cloudflare.Website.Waku` deploys a [Waku](https://waku.gg) app as a |
| 13 | Cloudflare Worker. It builds the project programmatically: the React |
| 14 | Server Components server bundle deploys as the Worker script, and the |
| 15 | client output — including SSG-prerendered pages — deploys as static |
| 16 | assets. No `waku.config.ts` edits are required — if you have one it |
| 17 | loads natively — and there is no Wrangler file and no build command |
| 18 | to run. |
| 19 | |
| 20 | ## Install |
| 21 | |
| 22 | The build integration is not bundled with alchemy. Install |
| 23 | `@alchemy.run/frontend-frameworks`; the resource loads its `/waku` |
| 24 | export from your project at deploy time. It is only used at build time, |
| 25 | so a dev dependency is enough: |
| 26 | |
| 27 | <Tabs syncKey="pkgManager"> |
| 28 | <TabItem label="bun" icon="bun"> |
| 29 | <Code code={`bun add -d ${devDeps}`} lang="sh" /> |
| 30 | </TabItem> |
| 31 | <TabItem label="npm" icon="npm"> |
| 32 | <Code code={`npm install -D ${devDeps}`} lang="sh" /> |
| 33 | </TabItem> |
| 34 | <TabItem label="pnpm" icon="pnpm"> |
| 35 | <Code code={`pnpm add -D ${devDeps}`} lang="sh" /> |
| 36 | </TabItem> |
| 37 | <TabItem label="yarn" icon="seti:yarn"> |
| 38 | <Code code={`yarn add -D ${devDeps}`} lang="sh" /> |
| 39 | </TabItem> |
| 40 | </Tabs> |
| 41 | |
| 42 | ## Configure Waku |
| 43 | |
| 44 | No framework config is required — a fresh Waku project deploys |
| 45 | as-is. If your project has a `waku.config.ts`, Alchemy loads it |
| 46 | natively (exactly as Waku's own CLI does) and uses it as the base |
| 47 | config — including Vite plugins under its `vite` field: |
| 48 | |
| 49 | ```typescript |
| 50 | // waku.config.ts |
| 51 | import { defineConfig } from "waku/config"; |
| 52 | import tsconfigPaths from "vite-tsconfig-paths"; |
| 53 | |
| 54 | export default defineConfig({ |
| 55 | basePath: "/", |
| 56 | vite: { |
| 57 | plugins: [tsconfigPaths()], |
| 58 | }, |
| 59 | }); |
| 60 | ``` |
| 61 | |
| 62 | `waku.config.*` is the home for static configuration. For deploy-time |
| 63 | values — anything that varies by stage — the `waku` prop merges |
| 64 | overrides over the file, per key: |
| 65 | |
| 66 | ```typescript |
| 67 | export const Website = Cloudflare.Website.Waku("Website", { |
| 68 | waku: { |
| 69 | basePath: "/docs/", |
| 70 | }, |
| 71 | }); |
| 72 | ``` |
| 73 | |
| 74 | The one key Alchemy owns is `unstable_adapter` — setting it fails the |
| 75 | build with an actionable error. |
| 76 | |
| 77 | :::note |
| 78 | Vite configuration must live in `waku.config.ts`'s `vite` field — |
| 79 | a standalone `vite.config.ts` is not loaded, same as Waku's own CLI. |
| 80 | ::: |
| 81 | |
| 82 | ## Declare the Website |
| 83 | |
| 84 | Declare the site as a module-level const (rather than inline in the |
| 85 | Stack) and derive the typed shape of its bindings from it: |
| 86 | |
| 87 | ```typescript |
| 88 | // alchemy.run.ts |
| 89 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 90 | |
| 91 | export const Website = Cloudflare.Website.Waku("Website"); |
| 92 | |
| 93 | export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>; |
| 94 | ``` |
| 95 | |
| 96 | `WebsiteEnv` is the typed shape of the Worker's bindings, derived |
| 97 | from the class — you'll import it into your Waku code in |
| 98 | [Read bindings in server code](#read-bindings-in-server-code). |
| 99 | |
| 100 | ## Add it to the Stack |
| 101 | |
| 102 | Yield the class from your Stack and return its URL: |
| 103 | |
| 104 | ```typescript |
| 105 | // alchemy.run.ts |
| 106 | import * as Alchemy from "alchemy"; |
| 107 | import * as Effect from "effect/Effect"; |
| 108 | |
| 109 | export default Alchemy.Stack( |
| 110 | "MyWakuSite", |
| 111 | { |
| 112 | providers: Cloudflare.providers(), |
| 113 | state: Cloudflare.state(), |
| 114 | }, |
| 115 | Effect.gen(function* () { |
| 116 | const site = yield* Website; |
| 117 | return { url: site.url }; |
| 118 | }), |
| 119 | ); |
| 120 | ``` |
| 121 | |
| 122 | Waku's server runtime uses `AsyncLocalStorage`, so the `nodejs_als` |
| 123 | compatibility flag is added automatically — you don't need to pass |
| 124 | it. SSG pages are served at their extensionless URLs (`/about`) via |
| 125 | the default `drop-trailing-slash` asset handling. |
| 126 | |
| 127 | During `alchemy dev`, Waku's dev server runs with SSR executing in |
| 128 | workerd, so server code sees the same runtime and the same real |
| 129 | bindings it will have in production. |
| 130 | |
| 131 | See |
| 132 | [examples/cloudflare-website-waku](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-waku) |
| 133 | for the checked-in example. |
| 134 | |
| 135 | ## Add bindings |
| 136 | |
| 137 | The resource returns a plain `Worker`, so `env` accepts the full |
| 138 | binding vocabulary — KV namespaces, R2 buckets, Durable Objects, |
| 139 | secrets: |
| 140 | |
| 141 | ```diff lang="typescript" |
| 142 | // alchemy.run.ts |
| 143 | +import * as Config from "effect/Config"; |
| 144 | |
| 145 | +export const Uploads = Cloudflare.R2.Bucket("Uploads"); |
| 146 | |
| 147 | export const Website = Cloudflare.Website.Waku("Website", { |
| 148 | + env: { |
| 149 | + UPLOADS: Uploads, |
| 150 | + API_KEY: Config.Redacted("API_KEY"), |
| 151 | + }, |
| 152 | }); |
| 153 | ``` |
| 154 | |
| 155 | `Uploads` is a description, not a deploy — Alchemy provisions the |
| 156 | real bucket because the Website binds it. `Config.Redacted` reads |
| 157 | `API_KEY` from your environment at deploy time and binds it as a |
| 158 | Worker secret — see [Secrets & env](/cloudflare/security/secrets-env). |
| 159 | |
| 160 | ## Read bindings in server code |
| 161 | |
| 162 | Server components and API routes read bindings from the |
| 163 | `cloudflare:workers` env at request time. Use a guarded dynamic |
| 164 | import — Waku's SSG step renders static pages in Node, where a |
| 165 | top-level `import { env } from "cloudflare:workers"` cannot |
| 166 | resolve — and type the result with the inferred env type: |
| 167 | |
| 168 | ```typescript |
| 169 | // src/lib/env.ts |
| 170 | import type { WebsiteEnv } from "../../alchemy.run.ts"; |
| 171 | |
| 172 | export const getEnv = async (): Promise<WebsiteEnv> => { |
| 173 | const { env } = await import("cloudflare:workers"); |
| 174 | return env as WebsiteEnv; |
| 175 | }; |
| 176 | ``` |
| 177 | |
| 178 | Server code calls it per request: |
| 179 | |
| 180 | ```typescript |
| 181 | // src/pages/api/upload.ts |
| 182 | import { getEnv } from "../../lib/env.ts"; |
| 183 | |
| 184 | export const POST = async (request: Request) => { |
| 185 | const env = await getEnv(); |
| 186 | await env.UPLOADS.put("hello.txt", await request.text()); |
| 187 | return Response.json({ ok: true }); |
| 188 | }; |
| 189 | ``` |
| 190 | |
| 191 | `env.UPLOADS` is typed as an R2 bucket and `env.API_KEY` as a |
| 192 | string — renaming a binding in `alchemy.run.ts` is a type error in |
| 193 | your server code. |
| 194 | |
| 195 | ## Custom Worker entry (Durable Objects) |
| 196 | |
| 197 | By default the deployed Worker entry is Waku's own RSC server entry. |
| 198 | When the Worker must export more than Waku's fetch handler — Durable |
| 199 | Object classes, additional handlers — point `main` at your own module |
| 200 | that wraps Waku's handler and re-exports the extras: |
| 201 | |
| 202 | ```typescript |
| 203 | // src/worker-entry.ts |
| 204 | import wakuHandler from "virtual:waku/server-entry"; |
| 205 | export class Counter extends DurableObject {} |
| 206 | export default { |
| 207 | fetch: (request, env, ctx) => wakuHandler.fetch(request, env, ctx), |
| 208 | }; |
| 209 | ``` |
| 210 | |
| 211 | ```typescript |
| 212 | export const Website = Cloudflare.Website.Waku("Website", { |
| 213 | main: "src/worker-entry.ts", |
| 214 | env: { |
| 215 | COUNTER: Cloudflare.DurableObject("Counter", { |
| 216 | className: "Counter", |
| 217 | }), |
| 218 | }, |
| 219 | }); |
| 220 | ``` |