1---
2title: Waku
3description: 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.
4sidebar:
5 order: 12
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Cloudflare.Website.Waku` deploys a [Waku](https://waku.gg) app as a
13Cloudflare Worker. It builds the project programmatically: the React
14Server Components server bundle deploys as the Worker script, and the
15client output — including SSG-prerendered pages — deploys as static
16assets. No `waku.config.ts` edits are required — if you have one it
17loads natively — and there is no Wrangler file and no build command
18to run.
19
20## Install
21
22The build integration is not bundled with alchemy. Install
23`@alchemy.run/frontend-frameworks`; the resource loads its `/waku`
24export from your project at deploy time. It is only used at build time,
25so 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
44No framework config is required — a fresh Waku project deploys
45as-is. If your project has a `waku.config.ts`, Alchemy loads it
46natively (exactly as Waku's own CLI does) and uses it as the base
47config — including Vite plugins under its `vite` field:
48
49```typescript
50// waku.config.ts
51import { defineConfig } from "waku/config";
52import tsconfigPaths from "vite-tsconfig-paths";
53
54export 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
63values — anything that varies by stage — the `waku` prop merges
64overrides over the file, per key:
65
66```typescript
67export const Website = Cloudflare.Website.Waku("Website", {
68 waku: {
69 basePath: "/docs/",
70 },
71});
72```
73
74The one key Alchemy owns is `unstable_adapter` — setting it fails the
75build with an actionable error.
76
77:::note
78Vite configuration must live in `waku.config.ts`'s `vite` field —
79a standalone `vite.config.ts` is not loaded, same as Waku's own CLI.
80:::
81
82## Declare the Website
83
84Declare the site as a module-level const (rather than inline in the
85Stack) and derive the typed shape of its bindings from it:
86
87```typescript
88// alchemy.run.ts
89import * as Cloudflare from "alchemy/Cloudflare";
90
91export const Website = Cloudflare.Website.Waku("Website");
92
93export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
94```
95
96`WebsiteEnv` is the typed shape of the Worker's bindings, derived
97from 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
102Yield the class from your Stack and return its URL:
103
104```typescript
105// alchemy.run.ts
106import * as Alchemy from "alchemy";
107import * as Effect from "effect/Effect";
108
109export 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
122Waku's server runtime uses `AsyncLocalStorage`, so the `nodejs_als`
123compatibility flag is added automatically — you don't need to pass
124it. SSG pages are served at their extensionless URLs (`/about`) via
125the default `drop-trailing-slash` asset handling.
126
127During `alchemy dev`, Waku's dev server runs with SSR executing in
128workerd, so server code sees the same runtime and the same real
129bindings it will have in production.
130
131See
132[examples/cloudflare-website-waku](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-waku)
133for the checked-in example.
134
135## Add bindings
136
137The resource returns a plain `Worker`, so `env` accepts the full
138binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
139secrets:
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
147export 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
156real bucket because the Website binds it. `Config.Redacted` reads
157`API_KEY` from your environment at deploy time and binds it as a
158Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
159
160## Read bindings in server code
161
162Server components and API routes read bindings from the
163`cloudflare:workers` env at request time. Use a guarded dynamic
164import — Waku's SSG step renders static pages in Node, where a
165top-level `import { env } from "cloudflare:workers"` cannot
166resolve — and type the result with the inferred env type:
167
168```typescript
169// src/lib/env.ts
170import type { WebsiteEnv } from "../../alchemy.run.ts";
171
172export const getEnv = async (): Promise<WebsiteEnv> => {
173 const { env } = await import("cloudflare:workers");
174 return env as WebsiteEnv;
175};
176```
177
178Server code calls it per request:
179
180```typescript
181// src/pages/api/upload.ts
182import { getEnv } from "../../lib/env.ts";
183
184export 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
192string — renaming a binding in `alchemy.run.ts` is a type error in
193your server code.
194
195## Custom Worker entry (Durable Objects)
196
197By default the deployed Worker entry is Waku's own RSC server entry.
198When the Worker must export more than Waku's fetch handler — Durable
199Object classes, additional handlers — point `main` at your own module
200that wraps Waku's handler and re-exports the extras:
201
202```typescript
203// src/worker-entry.ts
204import wakuHandler from "virtual:waku/server-entry";
205export class Counter extends DurableObject {}
206export default {
207 fetch: (request, env, ctx) => wakuHandler.fetch(request, env, ctx),
208};
209```
210
211```typescript
212export 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```