--- slug: cloudflare title: Run Waku on Cloudflare description: How to integrate Waku with Cloudflare Workers and interact with Cloudflare bindings and other resources. category: Deployment order: 10 --- ## Quick Start Waku comes "out of the box" with a custom adapter for [Cloudflare Workers](https://developers.cloudflare.com/workers/). Create your project with `npm create waku@latest -- --example https://github.com/wakujs/waku-examples/tree/main/fs-router/cloudflare` to use the starter example 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 Waku integrates with Cloudflare Workers via [`@cloudflare/vite-plugin`](https://developers.cloudflare.com/workers/vite-plugin/). This plugin runs your code in the actual Cloudflare workerd runtime during both development and build, so Cloudflare bindings (D1, KV, etc.) work without additional shims. > `@cloudflare/vite-plugin` is optional. If you don't need Cloudflare-specific features like D1, KV, or other bindings, you can deploy to Cloudflare Workers without it; just use `waku/adapters/cloudflare` in your `src/waku.server.tsx`. Install `@cloudflare/vite-plugin` and [`wrangler`](https://www.npmjs.com/package/wrangler) as development dependencies: ```sh npm install --save-dev @cloudflare/vite-plugin wrangler ``` Create a `src/waku.server.tsx` file that uses the Cloudflare adapter: ```ts // ./src/waku.server.tsx import { fsRouter } from 'waku'; import adapter from 'waku/adapters/cloudflare'; export default adapter(fsRouter(import.meta.glob('./pages/**/*.{tsx,ts}'))); ``` Add `@cloudflare/vite-plugin` to your `waku.config.ts`: ```ts // ./waku.config.ts import { cloudflare } from '@cloudflare/vite-plugin'; import { defineConfig } from 'waku/config'; export default defineConfig({ vite: { environments: { rsc: { optimizeDeps: { include: ['hono/tiny'], }, build: { rolldownOptions: { platform: 'neutral', }, }, }, ssr: { optimizeDeps: { include: ['waku > rsc-html-stream/server'], }, build: { rolldownOptions: { platform: 'neutral', }, }, }, }, plugins: [ cloudflare({ viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }, inspectorPort: false, }), ], }, }); ``` Configure your `wrangler.jsonc` to point to the server entry: ```jsonc // ./wrangler.jsonc { "$schema": "node_modules/wrangler/config-schema.json", "name": "waku-project", "main": "./src/waku.server", "compatibility_flags": ["nodejs_als"], "compatibility_date": "2025-11-17", "assets": { "binding": "ASSETS", "directory": "./dist/public", "html_handling": "drop-trailing-slash", }, "rules": [ { "type": "ESModule", "globs": ["**/*.js", "**/*.mjs"], }, ], "no_bundle": true, } ``` See [Cloudflare's documentation](https://developers.cloudflare.com/workers/wrangler/configuration/) for more information on configuring `wrangler.jsonc`. After setting up, run `waku build` to build and `npx wrangler dev` to test locally, or `npx wrangler deploy` to deploy. ## Notes on Cloudflare's workerd Runtime Cloudflare does not run NodeJS on their servers. Instead, they use their custom JavaScript runtime called [workerd](https://github.com/cloudflare/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](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) and [compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#setting-compatibility-flags). Waku attempts to stay minimal and compatible with [WinterCG servers](https://wintercg.org/). 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](https://developers.cloudflare.com/workers/reference/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 examples repository](https://github.com/wakujs/waku-examples/tree/main/fs-router/cloudflare), 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/) for more information. For general Waku request context usage, see [Request Context](/guides/request-context). > Note: 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) from your Waku app. ```ts import { env, waitUntil } from 'cloudflare:workers'; // eslint-disable-line import/no-unresolved const getData = async (userId: string) => { waitUntil( new Promise((resolve) => { console.log('Waiting for 5 seconds'); setTimeout(() => { console.log('OK, done waiting'); resolve(); }, 5000); }), ); const { results } = await env.DB.prepare('SELECT * FROM user WHERE id = ?') .bind(userId) .all(); return results; }; ``` ### Dev Mode The `@cloudflare/vite-plugin` configured in `waku.config.ts` runs your code in the Cloudflare workerd runtime during local development, so Cloudflare bindings like KV, D1, etc. are available in your server components and functions without additional setup. See [Cloudflare's Vite plugin documentation](https://developers.cloudflare.com/workers/vite-plugin/) for more details. ## Additional Handlers Waku supports defining additional [handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/) for your worker. Pass them to the adapter options in your waku.server.tsx file: ```ts import { fsRouter } from 'waku'; import adapter from 'waku/adapters/cloudflare'; export default adapter(fsRouter(import.meta.glob('./pages/**/*.{tsx,ts}')), { handlers: { // Define additional Cloudflare Workers handlers here // https://developers.cloudflare.com/workers/runtime-apis/handlers/ // async queue( // batch: MessageBatch, // _env: Env, // _ctx: ExecutionContext, // ): Promise { // for (const message of batch.messages) { // console.log('Received', message); // } // }, } satisfies ExportedHandler, }); ``` ## 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. ```json { "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 middleware. For example, if you want to fetch HTML from static assets to render: ```ts 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`](https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first). ### Custom Headers You can set response headers from custom middleware on the Hono response (`c.res`). Since Cloudflare supports response body streaming, server components might not be able to set headers if they were already sent in the response stream. For static assets, add a [`_headers`](https://developers.cloudflare.com/workers/static-assets/headers/#custom-headers) file to the root of your `public` folder to set custom headers for static assets. > An example `_headers` file is included in Waku's starter template for Cloudflare Workers to prevent indexing of RSC files by search engines: `./public/_headers`: ```txt /RSC/* X-Robots-Tag: noindex ``` ### Static 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: ```json { "name": "waku-project", "compatibility_date": "2025-11-17", "assets": { "directory": "./dist/public", "html_handling": "drop-trailing-slash" } } ``` and a `src/waku.server.tsx` file that uses the Cloudflare adapter with the `static: true` option: ```ts import { fsRouter } from 'waku'; import adapter from 'waku/adapters/cloudflare'; export default adapter(fsRouter(import.meta.glob('./pages/**/*.{tsx,ts}')), { 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`: ```ts 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`.