Run Waku on Cloudflare
How to integrate Waku with Cloudflare Workers and interact with Cloudflare bindings and other resources.
Quick Start
Waku comes "out of the box" with a custom adapter for Cloudflare Workers.
Create your project with npm create waku@latest -- --template 07_cloudflare to use the starter template for Cloudflare Workers.
Then use these commands:
- npm run dev: start the development server
- npm run build: build for Cloudflare Workers
- npx wrangler dev: test your build locally
- npx wrangler deploy: deploy it to Cloudflare Workers.
Building Waku For Cloudflare Workers
To build a Waku app for Cloudflare Workers, simply set the CLOUDFLARE or WORKERS_CI environment variable to 1 and run waku build. Waku will use its adapter for Cloudflare Workers. Alternatively, you can import 'waku/adapters/cloudflare' in your src/waku.server.ts file.
./src/waku.server.ts:
import { fsRouter } from 'waku';
import adapter from 'waku/adapters/cloudflare';
export default adapter(
fsRouter(import.meta.glob('./**/*.{tsx,ts}', { base: './pages' })),
);NoteThe Waku starter template for Cloudflare Workers includes an example ./src/waku.server.ts file and sets the CLOUDFLARE environment variable in the build script in package.json:
{
"scripts": {
"build": "CLOUDFLARE=1 waku build"
}
}The first time you run npm run build, it will create a wrangler.jsonc file with minimal configuration for Cloudflare Workers. See Cloudflare's documentation for more information on configuring Cloudflare Workers and wrangler.jsonc.
NoteThe Waku starter template for Cloudflare Workers also includes an example wrangler.jsonc file with an example environment variable that sets env.MAX_ITEMS to 10.
To test your build, you can add Cloudflare's CLI tool wrangler as a development dependency:
npm install --save-dev wranglerAfter building, you can test your build by running npx wrangler dev or deploy it to Cloudflare using npx wrangler deploy.
Notes on Cloudflare's workerd Runtime
Cloudflare does not run NodeJS on their servers. Instead, they use their custom JavaScript runtime called workerd.
By default, workerd does not support built-in NodeJS APIs, but support can be added by editing the compatibility_flags in your wrangler.jsonc file. Cloudflare does not support all APIs, but the list is growing. For more information, see Cloudflare's documentation on NodeJS APIs and compatibility flags.
Waku attempts to stay minimal and compatible with WinterCG servers. The Node AsyncLocalStorage API is currently used by Waku, so only the nodejs_als compatibility flag is added. If you experience errors in server-side dependencies due to missing NodeJS APIs, try changing this flag to nodejs_compat and rebuilding your project.
Note that the latest nodejs_compat mocks the Node fs module. Cloudflare does not allow file system access from server-side functions. See Cloudflare's security model.
Setting Up TypeScript
You can run npx wrangler types to generate a worker-configuration.d.ts file based on the settings in your wrangler.jsonc. This defines a global Env interface with your bindings. In the Cloudflare example in the Waku GitHub repository, a package.json script is included to run this command and update the types: pnpm run cf-typegen. To ensure that your types are always up-to-date, make sure to run it after any changes to your wrangler.jsonc config file.
Accessing Cloudflare Bindings, Execution Context, and Request/Response Objects
Import from cloudflare:workers to access Cloudflare Workers bindings such as environment variables, D1 databases and KV namespaces. See Cloudflare's documentation on Workers bindings for more information.
NoteNote: Durable Objects cannot currently be defined in a Waku app. You can create Durable Objects in another Cloudflare Worker and connect to it via service bindings from your Waku app.
import { env, waitUntil } from 'cloudflare:workers'; // eslint-disable-line import/no-unresolved
import { unstable_getContext as getContext } from 'waku/server';
const getData = async () => {
const { req } = getContext();
waitUntil(
new Promise<void>((resolve) => {
console.log('Waiting for 5 seconds');
setTimeout(() => {
console.log('OK, done waiting');
resolve();
}, 5000);
}),
);
const url = new URL(req.url);
const userId = url.searchParams.get('userId');
if (!userId) {
return null;
}
const { results } = await env.DB.prepare('SELECT * FROM user WHERE id = ?')
.bind(userId)
.all();
return results;
};Dev Mode Setup
NoteNote: the following setup is optional. You can also develop Waku apps for Cloudflare Workers without it, but you won't be able to access Cloudflare bindings like KV, D1, etc. in your server components or functions during local development. If you create your project with the Waku Cloudflare Workers starter template, this setup is already done for you.
To be able to simulate the Cloudflare Workers environment locally, install @hiogawa/node-loader-cloudflare and Cloudflare's build tool, wrangler as development dependencies:
npm install --save-dev @hiogawa/node-loader-cloudflare wranglerThen, create a waku.config.ts file in your project root with the following content:
import nodeLoaderCloudflare from '@hiogawa/node-loader-cloudflare/vite';
import { defineConfig } from 'waku/config';
export default defineConfig({
vite: {
plugins: [
nodeLoaderCloudflare({
environments: ['rsc'],
build: true,
// https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy
getPlatformProxyOptions: {
persist: {
path: '.wrangler/state/v3',
},
},
}),
],
},
});Additional Handlers
Waku supports defined additional handlers for your worker. Pass them to the adapter options in your waku.server.ts file:
import { fsRouter } from 'waku';
import adapter from 'waku/adapters/cloudflare';
export default adapter(
fsRouter(import.meta.glob('./**/*.{tsx,ts}', { base: './pages' })),
{
handlers: {
// Define additional Cloudflare Workers handlers here
// https://developers.cloudflare.com/workers/runtime-apis/handlers/
// async queue(
// batch: MessageBatch,
// _env: Env,
// _ctx: ExecutionContext,
// ): Promise<void> {
// for (const message of batch.messages) {
// console.log('Received', message);
// }
// },
} satisfies ExportedHandler<Env>,
},
);Static vs. Dynamic Routing and Fetching Assets
When Waku builds for Cloudflare, it outputs the worker function assets into the dist/server folder and outputs static assets into the dist/public folder.
A configuration in the wrangler.jsonc file tells Cloudflare to route requests to that assets folder first and then fall back to handle the request with the worker.
{
"assets": {
"binding": "ASSETS",
"directory": "./dist/public",
"html_handling": "drop-trailing-slash"
}
}You can also access static assets from your server-side worker code in a server component, server function or Waku middleware. For example, if you want to fetch HTML from static assets to render:
import { env } from 'cloudflare:workers'; // eslint-disable-line import/no-unresolved
const get404Html = async () => {
return env.ASSETS
? await (await env.ASSETS.fetch('https://example.com/404.html')).text()
: '';
};Note that ASSETS.fetch requires a fully qualified URL, but the origin is ignored. You can use https://example.com or any valid origin. It is just an internal request.
It is also possible to always run the worker before serving static assets. See the documentation for run_worker_first.
Custom Headers
You can set response headers on the res object of the Waku context. Since Cloudflare supports response body streaming, server components might not be able to set headers if they were already sent in the response stream. Headers can be set from custom Waku middleware.
For static assets, add a _headers file to the root of your public folder to set custom headers for static assets.
NoteAn example _headers file is included in Waku's starter template for Cloudflare Workers to prevent indexing of RSC files by search engines:
./public/_headers:
/RSC/*
X-Robots-Tag: noindexStatic Apps Without A Worker
It is possible to deploy a static Waku app to Cloudflare Workers. It will deploy the static assets to Cloudflare's edge network without ever invoking any worker functions.
To do this, create a wrangler.jsonc file with the following content:
{
"name": "waku-project",
"compatibility_date": "2025-11-17",
"assets": {
"directory": "./dist/public",
"html_handling": "drop-trailing-slash"
}
}and a src/waku.server.ts file that uses the Cloudflare adapter with the static: true option:
import { fsRouter } from 'waku';
import adapter from 'waku/adapters/cloudflare';
export default adapter(
fsRouter(import.meta.glob('./**/*.{tsx,ts}', { base: './pages' })),
{ static: true },
);You must also make sure that all of your pages and layouts are defined as static by exporting a getConfig function that specifies render: 'static'. For example, in ./src/pages/index.tsx:
export const getConfig = async () => {
return {
render: 'static',
} as const;
};Then run npm run build to build the static assets into the dist/public folder, and deploy with npx wrangler deploy.

