React Router
React Router v7 builds through Vite, so
Cloudflare.Website.Vite deploys it — one
resource, no Wrangler config, no manual entrypoint. In React Server
Components mode the build emits multiple server environments (rsc
and ssr) instead of the single ssr environment most SSR frameworks
produce, and viteEnvironments is the prop that tells Alchemy how those
environments assemble into one Worker. The configuration on this page is
a supported, live-tested shape: Alchemy’s test suite deploys it to real
Cloudflare and asserts that server-rendered HTML and client routes both
serve.
Project layout
Section titled “Project layout”React Router’s RSC mode is wired directly on @vitejs/plugin-rsc, so
the per-environment entry modules live in your project:
.├── alchemy.run.ts # the Stack├── vite.config.ts├── app/ # root.tsx, routes.ts, routes/└── react-router-vite/ # entry.browser.tsx, entry.ssr.tsx, # entry.worker.tsx, worker-ssr.tsxapp/ holds ordinary React Router routes; react-router-vite/ holds
one entry module per Vite environment — the browser bundle, the SSR
renderer, and the Worker handler.
Install the dependencies
Section titled “Install the dependencies”React Server Components need React Router 7.16+ and the RSC Vite plugin:
bun add react react-dom react-routerbun add -d vite @vitejs/plugin-react @vitejs/plugin-rscnpm install react react-dom react-routernpm install -D vite @vitejs/plugin-react @vitejs/plugin-rscpnpm add react react-dom react-routerpnpm add -D vite @vitejs/plugin-react @vitejs/plugin-rscyarn add react react-dom react-routeryarn add -D vite @vitejs/plugin-react @vitejs/plugin-rsc@vitejs/plugin-rsc is what splits the build into client, ssr, and
rsc environments; @vitejs/plugin-react handles the usual JSX/HMR
transform.
Configure the RSC plugin
Section titled “Configure the RSC plugin”Point rsc() at one entry per environment:
import react from "@vitejs/plugin-react";import rsc from "@vitejs/plugin-rsc";import { defineConfig } from "vite";
export default defineConfig({ plugins: [ react(), rsc({ serverHandler: false, entries: { client: "./react-router-vite/entry.browser.tsx", ssr: "./react-router-vite/entry.ssr.tsx", rsc: "./react-router-vite/entry.worker.tsx", }, }), ], optimizeDeps: { include: ["react-router", "react-router/internal/react-server-client"], },});client is the browser bundle, ssr renders HTML, and rsc is the
Worker entry — a module that default-exports the { fetch } handler
Cloudflare invokes, which is also why serverHandler is off: the Worker
serves requests itself. Alchemy injects its Cloudflare Vite plugin on
top of this config at build and dev time, so there is nothing
Cloudflare-specific to add here.
Declare the environment inputs
Section titled “Declare the environment inputs”Give each server environment its own build input:
export default defineConfig({ plugins: [react(), rsc({ ... })], environments: { // The Worker is the RSC environment. rsc: { build: { rollupOptions: { input: { "entry.worker": "./react-router-vite/entry.worker.tsx" }, }, }, }, // A second ssr input the Worker loads on demand. ssr: { build: { rollupOptions: { input: { "worker-ssr": "./react-router-vite/worker-ssr.tsx" }, }, }, }, },});The rsc input is the Worker entry, and the extra ssr input
(worker-ssr) is a chunk the Worker loads across environments at
runtime via import.meta.viteRsc.loadModule("ssr", "worker-ssr") — the
pattern for Worker code that needs a non-react-server module such as
react-dom/server, which must not be imported directly from the rsc
entry.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the
Stack) and describe the environment topology with viteEnvironments:
import * as Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Vite("Website", { viteEnvironments: { entry: "rsc", children: ["ssr"], },});
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;No asset routing config is needed: the RSC build emits no
index.html (HTML is server-rendered), so page requests match no
static asset and fall through to the RSC handler, while the
hydration bundles serve directly from the asset layer. The server
bundle’s Node APIs are covered by the nodejs_compat compatibility
flag, which is enabled by default for every Worker.
Add it to the Stack
Section titled “Add it to the Stack”Yield the Website from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "ReactRouterApp", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const website = yield* Website; return { url: website.url }; }),);Add bindings
Section titled “Add bindings”The resource returns a plain Worker, so env accepts the full
binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
secrets:
import * as Config from "effect/Config";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export const Website = Cloudflare.Website.Vite("Website", { env: { UPLOADS: Uploads, API_KEY: Config.redacted("API_KEY"), }, viteEnvironments: { entry: "rsc", children: ["ssr"], },});Uploads is a description, not a deploy — Alchemy provisions the
real bucket because the Website binds it. Config.redacted reads
API_KEY from your environment at deploy time and binds it as a
Worker secret — see Secrets & env.
The Worker entry receives env as the standard second argument of
the module-worker fetch contract — type it with the inferred
shape:
import type { WebsiteEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WebsiteEnv, ctx: ExecutionContext) { // env.UPLOADS and env.API_KEY are fully typed here // ... hand off to the RSC request handler },};How viteEnvironments maps to the deploy
Section titled “How viteEnvironments maps to the deploy”viteEnvironments selects which of the build’s environments make up
the deployed Worker:
viteEnvironments: { entry: "rsc", // this environment's output is the Worker entry chunk children: ["ssr"], // these environments' chunks are bundled alongside it}The entry environment produces the chunk the Worker boots from, every
environment listed in children has its chunks bundled alongside so
cross-environment loadModule calls resolve inside the deployed Worker,
and the client environment is always deployed as static assets — it is
never listed. The default is { entry: "ssr", children: [] }, so a
React Router app that doesn’t use Server Components — a
single-environment SSR build — needs no viteEnvironments at all.
Deploy and verify
Section titled “Deploy and verify”bun alchemy deployThis exact shape is exercised by Alchemy’s live test suite (“Vite:
React Router RSC deploys from a distilled manifest”): the test deploys
the app to real Cloudflare and asserts the home route serves
server-rendered HTML, the /about client route resolves, and a
/worker-render route returns HTML rendered through the ssr
environment’s chunk — server rendering and client navigation are both
verified against the deployed Worker.