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

Waku integrates with Cloudflare Workers via @cloudflare/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.

Note

@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.ts.

Install @cloudflare/vite-plugin and wrangler as development dependencies:

npm install --save-dev @cloudflare/vite-plugin wrangler

Create a src/waku.server.ts file that uses the Cloudflare adapter:

// ./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' })),
);

Add @cloudflare/vite-plugin to your waku.config.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: {
          rollupOptions: {
            platform: 'neutral',
          } as never,
        },
      },
      ssr: {
        optimizeDeps: {
          include: ['waku > rsc-html-stream/server'],
        },
        build: {
          rollupOptions: {
            platform: 'neutral',
          } as never,
        },
      },
    },
    plugins: [
      cloudflare({
        viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
        inspectorPort: false,
      }),
    ],
  },
});

Configure your wrangler.jsonc to point to the server entry:

// ./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 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.

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.

Note

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 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

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 for more details.

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.

Note

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:

/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:

{
  "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.

designed bycandycode alternative graphic design web development agency San Diego