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.env instead of env bindings
  • A dependency uses require() which doesn’t exist in workerd
  • A dependency accesses fs or path
  • 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 workerd process via Miniflare
  • Cloudflare bindings (KV, D1, R2, etc.) are emulated locally with full fidelity
  • Compatibility flags from wrangler.jsonc are 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

FrameworkPlugin ConfigNotes
React Router v7cloudflare({ viteEnvironment: { name: "ssr" } })Worker entry in workers/app.ts
TanStack Startcloudflare({ viteEnvironment: { name: "ssr" } })Worker entry in workers/app.ts
Astro (v13+)Automatic via @astrojs/cloudflare adapterAdapter configures the plugin internally
Vite + React SPAcloudflare()Static assets only, no SSR environment
Honocloudflare()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.external configuration.
  • Not all frameworks supported: SvelteKit, Nuxt, Qwik, SolidStart, and Next.js don’t integrate with the CF Vite Plugin yet.
  • Extra process: Running workerd locally 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.