Cloudflare Vite Plugin
The @cloudflare/vite-plugin is the most important infrastructure shift in Cloudflare’s framework story. It bridges the gap between development (Node.js) and production (workerd) by running your server code in a local workerd instance during vite dev.
The Problem It Solves
Without the plugin, your development workflow looks like this:
sequenceDiagram
participant Dev as vite dev
participant Node as Node.js Runtime
participant Prod as wrangler deploy
participant WD as workerd Runtime
Dev->>Node: Run server code
Note over Node: Has fs, process, Buffer<br/>Has process.env<br/>Has require()
Prod->>WD: Run server code
Note over WD: No fs, no process<br/>Has env bindings<br/>ESM only
Note over Node,WD: Bugs hide until deploy
Code that works in vite dev can fail in production because:
- You used
process.envinstead of env bindings - A dependency uses
require()which doesn’t exist inworkerd - A dependency accesses
fsorpath - A CJS module isn’t compatible with the V8 isolate
How It Works
The CF Vite Plugin uses Vite’s Environment API (introduced in Vite 6) to create a separate execution environment that runs inside workerd.
flowchart TB
subgraph "Vite Dev Server"
A[Client Environment<br/>Browser]
B[SSR Environment<br/>workerd via Miniflare]
end
A -->|"HMR, Assets"| C[Browser]
B -->|"Server rendering,<br/>API routes"| D[Local workerd]
D -->|"Real bindings"| E[KV, D1, R2<br/>Local emulation]
Key details:
- The client environment (browser code, HMR) works as normal
- The SSR environment runs inside a local
workerdprocess via Miniflare - Cloudflare bindings (KV, D1, R2, etc.) are emulated locally with full fidelity
- Compatibility flags from
wrangler.jsoncare applied in dev
Installation
npm install -D @cloudflare/vite-plugin
Basic Configuration
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [cloudflare()],
});
For frameworks with their own SSR environment (React Router, TanStack Start):
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
// Tell CF plugin which Vite environment is the SSR one
cloudflare({ viteEnvironment: { name: "ssr" } }),
reactRouter(),
],
});
Framework Integration Map
| Framework | Plugin Config | Notes |
|---|---|---|
| React Router v7 | cloudflare({ viteEnvironment: { name: "ssr" } }) | Worker entry in workers/app.ts |
| TanStack Start | cloudflare({ viteEnvironment: { name: "ssr" } }) | Worker entry in workers/app.ts |
| Astro (v13+) | Automatic via @astrojs/cloudflare adapter | Adapter configures the plugin internally |
| Vite + React SPA | cloudflare() | Static assets only, no SSR environment |
| Hono | cloudflare() | Can use directly, but wrangler dev also works |
Frameworks without CF Vite Plugin support (SvelteKit, Nuxt, Qwik, SolidStart, Next.js) use their own adapters and run dev in Node.js.
What the Plugin Provides in Dev
Real Bindings
// This actually works in vite dev with the CF plugin
export async function loader({ context }) {
// Hits a real local KV emulation, not a mock
const value = await context.cloudflare.env.MY_KV.get("key");
return { value };
}
Without the plugin, you’d need getPlatformProxy() or mock the bindings yourself.
Runtime Parity
// This fails in Node.js but works in workerd
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
async increment() {
let value = (await this.ctx.storage.get("count")) || 0;
value++;
await this.ctx.storage.put("count", value);
return value;
}
}
With the CF Vite Plugin, this code runs in vite dev just like it would in production.
Compatibility Flag Enforcement
// wrangler.jsonc
{
"compatibility_flags": ["nodejs_compat"],
"compatibility_date": "2025-01-01"
}
The plugin reads these flags and applies them to the local workerd instance, so you see the same behavior in dev that you’ll get in prod.
Worker Entry Point Pattern
The CF Vite Plugin enables a powerful pattern: your Worker entry point can export both a fetch handler (for the web app) and additional exports (Durable Objects, queue consumers, scheduled handlers).
// workers/app.ts
import { createRequestHandler } from "react-router";
import { DurableObject } from "cloudflare:workers";
// React Router handles web requests
const requestHandler = createRequestHandler(
() => import("virtual:react-router/server-build"),
import.meta.env.MODE
);
// Durable Object lives alongside the app
export class ChatRoom extends DurableObject {
async fetch(request: Request) {
// WebSocket handling, etc.
}
}
// Queue consumer
export default {
async fetch(request: Request, env: Env) {
return requestHandler(request, { cloudflare: { env } });
},
async queue(batch, env) {
for (const message of batch.messages) {
// Process queue messages
}
},
} satisfies ExportedHandler<Env>;
Tip: This colocation is only possible with the CF Vite Plugin. Adapter-based frameworks (SvelteKit, Nuxt) don’t give you access to the Worker entry point.
Limitations
- Vite 6+ required: The Environment API is a Vite 6 feature. Projects on Vite 5 can’t use this plugin.
- CJS compatibility: Some CJS dependencies may need pre-compilation or
vite.ssr.externalconfiguration. - Not all frameworks supported: SvelteKit, Nuxt, Qwik, SolidStart, and Next.js don’t integrate with the CF Vite Plugin yet.
- Extra process: Running
workerdlocally uses more resources than plain Node.js dev.
The Alternative: getPlatformProxy()
For frameworks without CF Vite Plugin support, wrangler provides getPlatformProxy() as a partial solution:
// Only needed for frameworks WITHOUT the CF Vite Plugin
import { getPlatformProxy } from "wrangler";
const { env } = await getPlatformProxy();
// env.MY_KV, env.MY_DB, etc. are available
// But your server code still runs in Node.js
This gives you binding access in dev, but your code still runs in Node.js - so you won’t catch workerd-specific runtime errors.
Gotcha:
getPlatformProxy()is a stopgap, not a replacement for the CF Vite Plugin. It provides bindings but not runtime parity.