adapter-cloudflare

Cloudflare Workers adapter for Catmint. Transforms Catmint build output into a Cloudflare Workers-compatible bundle deployable with wrangler.

Import

import cloudflare from "@catmint/adapter-cloudflare";

Signature

function cloudflareAdapter(
  options?: CloudflareAdapterOptions,
): CloudflareAdapter;

The default export is the cloudflareAdapter factory function.

CloudflareAdapter

The return type extends CatmintAdapter with an additional vitePlugin property:

interface CloudflareAdapter extends CatmintAdapter {
  vitePlugin: Plugin[];
}
PropertyTypeDescription
vitePluginPlugin[]Vite plugins that enable workerd dev mode. Add this to vite.plugins in your config to run your app in workerd.

Parameters

ParameterTypeRequiredDescription
optionsCloudflareAdapterOptionsNoConfiguration for the Cloudflare Workers deployment.

CloudflareAdapterOptions

PropertyTypeDefaultDescription
namestring"catmint-app"Worker name used in the generated wrangler.jsonc.
routesstring[]['/*']Worker route patterns.
compatibilityDatestringCurrent dateCloudflare Workers compatibility date. Defaults to today's date in YYYY-MM-DD format.
maxBodySizenumber1048576Maximum request body size in bytes (default 1 MB). Requests exceeding this limit receive a 413 Payload Too Large response.
kvNamespacesKVNamespaceBinding[]--KV Namespace bindings. See Bindings.
r2BucketsR2BucketBinding[]--R2 bucket bindings. See Bindings.
d1DatabasesD1DatabaseBinding[]--D1 database bindings. See Bindings.
durableObjectsDurableObjectBinding[]--Durable Object bindings. See Bindings.
wranglerRecord<string, unknown>--Raw wrangler config merged into the generated wrangler.jsonc. Overrides any generated values.

Return Value

Returns a CloudflareAdapter object with name: '@catmint/adapter-cloudflare' and a vitePlugin array. The adapter operates in one of two modes depending on whether vitePlugin is added to your Vite config:

Plugin Mode

When cf.vitePlugin is included in vite.plugins, the adapter uses @cloudflare/vite-plugin to run your RSC and SSR environments inside the actual workerd runtime during development. At build time, a pre-compiled TypeScript worker entry is used. This mode gives you the highest fidelity Cloudflare development experience.

Legacy Mode

When cf.vitePlugin is not included, the dev server runs in Node.js with getPlatformProxy() providing emulated bindings. At build time, the worker entry is generated from a string template. This mode requires only wrangler (not @cloudflare/vite-plugin).

In both modes, when the adapt() method is called during build, it:

  1. Generates worker.js -- the Workers entry point
  2. Generates wrangler.jsonc -- Wrangler configuration with the specified name, compatibility date, routes, and bindings
  3. Copies client assets and pre-rendered pages to public/
  4. Copies the server bundle into the output directory
  5. If the build manifest has hasMiddleware: true, middleware execution is included in the Worker handler. A prerendered.js module is generated that inlines middleware-protected pre-rendered HTML as a pathname-to-string map, since Workers cannot access the filesystem.

Output is written to dist/cloudflare/.

Frontend Mode

When the application is built with mode: 'frontend', the Cloudflare adapter skips worker generation entirely and only copies the static client assets. No worker.js is produced -- deploy the output as a static site to Cloudflare Pages or any static hosting provider.

Edge Compatibility Validation

During the build, the adapter scans server function source paths for Node.js-specific module imports (fs, child_process, node:, net, tls, etc.) and logs warnings for potential runtime incompatibilities. The Cloudflare Workers runtime does not support most Node.js built-in modules.

Security Headers

The adapter automatically applies baseline security headers (X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: SAMEORIGIN) to all responses, including static assets served via env.ASSETS. These are applied before any middleware-set headers.

Bindings

Cloudflare bindings connect your Worker to platform services like KV, R2, D1, and Durable Objects. Configure bindings in the adapter options and they are emitted into wrangler.jsonc.

KV Namespaces

cloudflare({
  kvNamespaces: [{ binding: "MY_KV", id: "abc123def456" }],
});
FieldTypeRequiredDescription
bindingstringYesThe binding name in code
idstringYesKV namespace ID

R2 Buckets

cloudflare({
  r2Buckets: [{ binding: "MY_BUCKET", bucketName: "my-bucket" }],
});
FieldTypeRequiredDescription
bindingstringYesThe binding name in code
bucketNamestringYesR2 bucket name

D1 Databases

cloudflare({
  d1Databases: [
    { binding: "DB", databaseName: "mydb", databaseId: "def456ghi789" },
  ],
});
FieldTypeRequiredDescription
bindingstringYesThe binding name in code
databaseNamestringYesD1 database name
databaseIdstringYesD1 database ID

Durable Objects

cloudflare({
  durableObjects: [
    { name: "COUNTER", className: "Counter" },
    { name: "CHAT_ROOM", className: "ChatRoom", scriptName: "chat-worker" },
  ],
});
FieldTypeRequiredDescription
namestringYesThe binding name in code
classNamestringYesDurable Object class name
scriptNamestringNoWorker script name (for external Durable Objects)

Wrangler Passthrough

For bindings not covered by the typed fields (Queues, Vectorize, AI, Hyperdrive, etc.), use the wrangler passthrough option. This object is merged directly into the generated wrangler.jsonc:

cloudflare({
  wrangler: {
    ai: { binding: "AI" },
    queues: {
      producers: [{ binding: "MY_QUEUE", queue: "my-queue" }],
    },
    hyperdrive: [{ binding: "HYPERDRIVE", id: "abc123" }],
  },
});

The wrangler passthrough is merged last and overrides any generated values with the same key. Use it carefully to avoid overwriting bindings set via the typed options.

Workerd Dev Mode (Vite Plugin)

The Cloudflare adapter exposes a vitePlugin property that integrates @cloudflare/vite-plugin into your Vite config. When enabled, the RSC and SSR Vite environments run inside the real workerd runtime during development, giving you full Cloudflare Workers API compatibility without emulation.

Setup

Install @cloudflare/vite-plugin as a dev dependency:

pnpm add -D @cloudflare/vite-plugin

Then instantiate the adapter and pass both the adapter and its vitePlugin into your config:

// catmint.config.ts
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";

const cf = cloudflare({ compatibilityDate: "2026-01-01" });

export default defineConfig({
  mode: "fullstack",
  adapter: cf,
  vite: {
    plugins: [cf.vitePlugin],
  },
});

@cloudflare/vite-plugin is an optional peer dependency. If it is not installed, the vitePlugin array is inert -- it will not error, but workerd dev mode will not activate. The adapter falls back to legacy dev mode using getPlatformProxy().

How It Works

  1. The bridge plugin sets an internal signal during Vite's config hook, telling the adapter that plugin mode is active
  2. The loader plugin dynamically imports @cloudflare/vite-plugin and configures it to take over the ssr environment with rsc as a child environment
  3. During catmint dev, both RSC and SSR code run inside a local workerd process instead of Node.js
  4. All bindings configured in the adapter options are passed to the workerd config automatically

When to Use It

Use plugin mode when you need:

  • Accurate Cloudflare Workers API behavior during development (e.g., caches, crypto.subtle, HTMLRewriter)
  • To test Workers-specific runtime constraints (no Node.js built-in modules)
  • High fidelity between your dev and production environments

If you only need basic bindings (KV, R2, D1) during development, legacy dev mode with getPlatformProxy() is simpler and does not require @cloudflare/vite-plugin.

Local Development

When cf.vitePlugin is not in your Vite plugins (or @cloudflare/vite-plugin is not installed), the adapter falls back to legacy dev mode. When you run catmint dev, the adapter's dev() hook starts a local emulation of Cloudflare bindings using Wrangler's getPlatformProxy(). This means getPlatform() returns real, working KV, R2, D1, and Durable Object bindings backed by a local workerd process -- no mocks or stubs needed.

Requirements

wrangler must be installed as a dev dependency:

pnpm add -D wrangler

If wrangler is not installed, the dev server throws an error with install instructions.

What You Get in Dev

getPlatform() returns the same shape as production:

const platform = getPlatform<CloudflarePlatform>();
// platform.env  — locally emulated bindings (KV, R2, D1, etc.)
// platform.ctx  — execution context
// platform.cf   — cf properties
// platform.caches — cache API

Bindings configured in the adapter options (kvNamespaces, d1Databases, r2Buckets, durableObjects, wrangler passthrough) are automatically available in both catmint dev and production builds -- catmint.config.ts is the single source of truth. No separate wrangler.toml is needed.

If no bindings are configured in the adapter options, getPlatformProxy() falls back to reading from a wrangler.toml / wrangler.jsonc in the project root.

The local proxy is automatically cleaned up when the dev server shuts down.

How It Works

  1. At startup, the adapter dynamically imports wrangler and calls getPlatformProxy()
  2. If bindings are configured in the adapter options, a temporary wrangler.jsonc is generated and passed to getPlatformProxy() via its configPath option
  3. This starts a local workerd process with your configured bindings
  4. On each request, the dev server calls the adapter's getPlatform() with the incoming request
  5. The adapter returns { env, ctx, cf, caches } from the proxy
  6. The dev server stores this in the request context, making it available via getPlatform()

Accessing Bindings at Runtime

Use getPlatform() from catmint/server to access Cloudflare bindings in server functions, middleware, endpoints, and RSC rendering:

import { createServerFn } from "catmint/server";
import { getPlatform } from "catmint/server";

interface CloudflarePlatform {
  env: {
    MY_KV: KVNamespace;
    MY_BUCKET: R2Bucket;
    DB: D1Database;
  };
  ctx: ExecutionContext;
}

export const getData = createServerFn(async () => {
  const { env } = getPlatform<CloudflarePlatform>();
  const value = await env.MY_KV.get("key");
  return { value };
});

See getPlatform for full documentation and platform-specific types.

Examples

// catmint.config.ts -- with workerd dev mode (recommended)
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";

const cf = cloudflare({ compatibilityDate: "2026-01-01" });

export default defineConfig({
  mode: "fullstack",
  adapter: cf,
  vite: {
    plugins: [cf.vitePlugin],
  },
});
// catmint.config.ts -- defaults (legacy mode)
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";

export default defineConfig({
  adapter: cloudflare(),
});
// Custom name, bindings, and compatibility date
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";

export default defineConfig({
  adapter: cloudflare({
    name: "my-app",
    compatibilityDate: "2026-01-01",
    kvNamespaces: [{ binding: "CACHE", id: "abc123" }],
    d1Databases: [
      { binding: "DB", databaseName: "mydb", databaseId: "def456" },
    ],
  }),
});

After building, deploy with:

npx wrangler deploy --config dist/cloudflare/wrangler.jsonc

See Also