Waku
Cloudflare.Website.Waku deploys a Waku app as a
Cloudflare Worker. It builds the project programmatically: the React
Server Components server bundle deploys as the Worker script, and the
client output — including SSG-prerendered pages — deploys as static
assets. There is no waku.config.ts to edit, no Wrangler file, and
no build command to run.
Install
Section titled “Install”The build integration is not bundled with alchemy — the resource
dynamically imports @distilled.cloud/waku from your project at
deploy time, so it must be installed alongside your framework. It is
only used at build time, so a dev dependency is enough:
bun add -d @distilled.cloud/wakunpm install -D @distilled.cloud/wakupnpm add -D @distilled.cloud/wakuyarn add -D @distilled.cloud/wakuConfigure Waku
Section titled “Configure Waku”There is no framework config to write: the resource builds the
project programmatically, so a fresh Waku project deploys as-is —
your waku.config.ts stays untouched.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:
import * as Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Waku("Website");
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;WebsiteEnv is the typed shape of the Worker’s bindings, derived
from the class — you’ll import it into your Waku code in
Read bindings in server code.
Add it to the Stack
Section titled “Add it to the Stack”Yield the class from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyWakuSite", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);Waku’s server runtime uses AsyncLocalStorage, so the nodejs_als
compatibility flag is added automatically — you don’t need to pass
it. SSG pages are served at their extensionless URLs (/about) via
the default drop-trailing-slash asset handling.
During alchemy dev, Waku’s dev server runs with SSR executing in
workerd, so server code sees the same runtime and the same real
bindings it will have in production.
See examples/cloudflare-website-waku for the checked-in example.
Add bindings
Section titled “Add bindings”The resource returns a plain Worker, so env accepts the full
binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
secrets:
import * as Config from "effect/Config";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export const Website = Cloudflare.Website.Waku("Website", { env: { UPLOADS: Uploads, API_KEY: Config.redacted("API_KEY"), },});Uploads is a description, not a deploy — Alchemy provisions the
real bucket because the Website binds it. Config.redacted reads
API_KEY from your environment at deploy time and binds it as a
Worker secret — see Secrets & env.
Read bindings in server code
Section titled “Read bindings in server code”Server components and API routes read bindings from the
cloudflare:workers env at request time. Use a guarded dynamic
import — Waku’s SSG step renders static pages in Node, where a
top-level import { env } from "cloudflare:workers" cannot
resolve — and type the result with the inferred env type:
import type { WebsiteEnv } from "../../alchemy.run.ts";
export const getEnv = async (): Promise<WebsiteEnv> => { const { env } = await import("cloudflare:workers"); return env as WebsiteEnv;};Server code calls it per request:
import { getEnv } from "../../lib/env.ts";
export const POST = async (request: Request) => { const env = await getEnv(); await env.UPLOADS.put("hello.txt", await request.text()); return Response.json({ ok: true });};env.UPLOADS is typed as an R2 bucket and env.API_KEY as a
string — renaming a binding in alchemy.run.ts is a type error in
your server code.
Custom Worker entry (Durable Objects)
Section titled “Custom Worker entry (Durable Objects)”By default the deployed Worker entry is Waku’s own RSC server entry.
When the Worker must export more than Waku’s fetch handler — Durable
Object classes, additional handlers — point main at your own module
that wraps Waku’s handler and re-exports the extras:
import wakuHandler from "virtual:waku/server-entry";export class Counter extends DurableObject {}export default { fetch: (request, env, ctx) => wakuHandler.fetch(request, env, ctx),};export const Website = Cloudflare.Website.Waku("Website", { main: "src/worker-entry.ts", env: { COUNTER: Cloudflare.DurableObject("Counter", { className: "Counter", }), },});