Skip to main content

cloudflareDevProxy

Provides Cloudflare Workers bindings to your loaders and actions during local development.

Import

import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";

Usage

import { reactRouter } from "@react-router/dev/vite";
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflareDevProxy(),
    reactRouter(),
  ],
});
The cloudflareDevProxy plugin must be placed before the reactRouter plugin.

Options

getLoadContext
function
Custom function to provide load context to your loaders and actions.
type GetLoadContext = (args: {
  request: Request;
  context: {
    cloudflare: {
      env: Env;
      cf: IncomingRequestCfProperties;
      ctx: ExecutionContext;
    };
  };
}) => AppLoadContext | Promise<AppLoadContext>;
Example:
cloudflareDevProxy({
  getLoadContext({ context }) {
    return {
      ...context,
      db: createDatabase(context.cloudflare.env.DB),
    };
  },
})
persist
boolean | { path: string }
Enables persistent storage for Cloudflare bindings (KV, DO, R2, D1).Default: true (persists to .wrangler/state/v3)
cloudflareDevProxy({
  persist: {
    path: "./.wrangler/state",
  },
})
configPath
string
Path to wrangler.toml configuration file.Default: "./wrangler.toml"
cloudflareDevProxy({
  configPath: "./cloudflare/wrangler.toml",
})
experimentalJsonConfig
boolean
Use wrangler.json instead of wrangler.toml.Default: false
cloudflareDevProxy({
  experimentalJsonConfig: true,
  configPath: "./wrangler.json",
})

Configuration

TypeScript Types

Define your Cloudflare environment types:
import type { PlatformProxy } from "wrangler";

export interface Env {
  DB: D1Database;
  KV: KVNamespace;
  BUCKET: R2Bucket;
  API_KEY: string;
}

export interface LoadContext {
  cloudflare: Omit<PlatformProxy<Env>, "dispose">;
}

declare module "react-router" {
  interface AppLoadContext extends LoadContext {}
}

wrangler.toml

Configure your Cloudflare bindings:
name = "my-app"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "KV"
id = "your-kv-namespace-id"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"

[vars]
API_KEY = "dev-api-key"

Common Patterns

Accessing Bindings in Loaders

import type { Route } from "./+types/products";

export async function loader({ context }: Route.LoaderArgs) {
  const { env } = context.cloudflare;
  
  // D1 Database
  const products = await env.DB
    .prepare("SELECT * FROM products")
    .all();
  
  // KV Storage
  const cached = await env.KV.get("products-cache");
  
  // R2 Storage
  const images = await env.BUCKET.list();
  
  // Environment variables
  const apiKey = env.API_KEY;
  
  return { products, cached, images };
}

Accessing Bindings in Actions

import type { Route } from "./+types/products.new";

export async function action({ request, context }: Route.ActionArgs) {
  const { env } = context.cloudflare;
  const formData = await request.formData();
  
  const product = {
    name: formData.get("name"),
    price: formData.get("price"),
  };
  
  // Insert into D1
  await env.DB
    .prepare("INSERT INTO products (name, price) VALUES (?, ?)")
    .bind(product.name, product.price)
    .run();
  
  // Invalidate cache
  await env.KV.delete("products-cache");
  
  return { success: true };
}

Custom Load Context

Extend the context with custom utilities:
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { createDb } from "./app/db.server";
import type { Env } from "./load-context";

export default defineConfig({
  plugins: [
    cloudflareDevProxy<Env>({
      getLoadContext({ context }) {
        return {
          ...context,
          db: createDb(context.cloudflare.env.DB),
          auth: createAuthService(context.cloudflare.env),
        };
      },
    }),
    reactRouter(),
  ],
});
import type { Route } from "./+types/admin";

export async function loader({ context }: Route.LoaderArgs) {
  // Use custom utilities
  const user = await context.auth.getCurrentUser();
  const data = await context.db.query("SELECT * FROM admin");
  
  return { user, data };
}

Durable Objects

[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
script_name = "my-app"
import type { Route } from "./+types/counter";

export async function loader({ context }: Route.LoaderArgs) {
  const { env } = context.cloudflare;
  const id = env.COUNTER.idFromName("global-counter");
  const stub = env.COUNTER.get(id);
  const response = await stub.fetch("/increment");
  const count = await response.json();
  
  return { count };
}

Service Bindings

[[services]]
binding = "API"
service = "my-api-worker"
import type { Route } from "./+types/api-proxy";

export async function loader({ request, context }: Route.LoaderArgs) {
  const { env } = context.cloudflare;
  const response = await env.API.fetch(request);
  const data = await response.json();
  
  return data;
}

Production Deployment

In production, Cloudflare provides actual bindings. The dev proxy is only for local development.

Build Configuration

import { reactRouter } from "@react-router/dev/vite";
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflareDevProxy(), // Only active in development
    reactRouter(),
  ],
  ssr: {
    resolve: {
      externalConditions: ["workerd", "worker"],
    },
  },
});

Deploy to Cloudflare Pages

npm run build
npx wrangler pages deploy ./build/client

Troubleshooting

Bindings Not Available

Ensure:
  1. Plugin is before reactRouter()
  2. wrangler.toml is properly configured
  3. Types are properly declared

D1 Database Issues

Create local D1 database:
npx wrangler d1 create my-database
npx wrangler d1 execute my-database --local --file=./schema.sql

KV Persistence

Data persists to .wrangler/state/v3 by default. Clear with:
rm -rf .wrangler/state

Build docs developers (and LLMs) love