A minimal template for running React Server Components on Cloudflare Workers with Hono, powered by @vitejs/plugin-rsc.
Related article: Zenn - Hono × RSC on Cloudflare Workers (coming soon)
- React 19 — Server Components + Streaming SSR
- Hono — Handles all routing (pages + API)
@vitejs/plugin-rsc— RSC protocol implementation (Vite 6 Environment API)- Cloudflare Workers — edge runtime
bun install
bun run dev # start dev server (Vite + HMR)
bun run build # production build
bun run preview # wrangler dev --local
bun run deploy # deploy to Cloudflare Workers@vitejs/plugin-rsc manages three separate build environments:
| Environment | Role | Runtime |
|---|---|---|
rsc |
RSC rendering + all routing | Cloudflare Workers (workerd) |
ssr |
Convert RSC stream → HTML | Node.js (dev) / Workers (prod) |
client |
Hydration | Browser |
RSC is implemented as a Hono middleware (rscMiddleware).
This means all routing — pages and API — lives in one Hono app.
entry.rsc.tsx entry.ssr.tsx entry.browser.tsx
│ │ │
│ handler(request) │ │
│ │ │
│ app.fetch(request) │ │
│ (Hono with rscMiddleware)│ │
│ │ │ │
│ ┌───────▼────────┐ │ │
│ │ GET / │ │ │
│ │ rscMiddleware │ │ │
│ │ renderPage( │ │ │
│ │ request, │ │ │
│ │ loader, │ │ │
│ │ isRsc=false │ │ │
│ │ ) │ │ │
│ └───────┬────────┘ │ │
│ │ RSC stream │ │
│ └────────────────► handleSsr() │
│ │ → HTML stream │
│ │
│ ┌───────▼────────┐ │
│ │ GET /__rsc/ │ │
│ │ rscMiddleware │ │
│ │ renderPage( │ │
│ │ request, │ │
│ │ loader, │ │
│ │ isRsc=true │ │
│ │ ) │ │
│ └───────┬────────┘ │
│ │ RSC stream → Response │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ GET /api/hello → c.json({...}) │ │
│ └───────────────────────────────────────────┘ │
Initial page load (HTML)
Browser → GET /
→ entry.rsc.tsx: handler → app.fetch()
→ rscMiddleware: inject renderPage into context
→ GET / handler: renderPage(request, HomePageLoader, false)
→ renderPage: renderToReadableStream(<HomePage />) → RSC stream
→ entry.ssr.tsx: createFromReadableStream() + renderToReadableStream()
→ Response: Content-Type: text/html
Hydration
Browser → GET /__rsc/ (bootstrapScriptContent triggers this)
→ entry.rsc.tsx: handler → app.fetch()
→ rscMiddleware: inject renderPage into context
→ GET /__rsc/ handler: renderPage(request, HomePageLoader, true)
→ renderPage: return RSC stream directly
→ Response: Content-Type: text/x-component
Browser: createFromReadableStream(body) → hydrateRoot(document, root)
RSC requires a way to distinguish "give me HTML" from "give me the RSC payload" for the same URL. Existing frameworks take different approaches:
| Framework | Method | CDN Cache | Spoofing Risk |
|---|---|---|---|
| Next.js | Rsc: 1 request header |
Needs Vary: Rsc |
Documented risk |
| Waku | /RSC/ path prefix |
Separate URLs | None (different path) |
| This template | /__rsc/ path prefix |
Separate URLs | None (different path) |
We chose the /__rsc/ path prefix approach, inspired by Waku's use of /RSC/:
- No spoofing — RSC and HTML are served from entirely different paths; no header stripping needed
- Natural CDN cache separation — different URLs = different cache entries, no
Varyheader required - Explicit routing —
/__rsc/*routes are registered explicitly in Hono, making the data flow easy to follow - isRsc passed by caller — route handlers decide
isRsc, not middleware heuristics
src/
├── framework/
│ ├── entry.rsc.tsx # RSC env — rscMiddleware, handler
│ ├── entry.ssr.tsx # SSR env — RSC stream → HTML
│ └── entry.browser.tsx # Browser — /__rsc/ fetch + hydrateRoot
├── lib/
│ ├── markdown/ # Markdown → React rendering (frontmatter, components)
│ └── router/ # File-based route resolver & runtime
├── routes/
│ ├── about/ # /about page (index.tsx + layout.tsx)
│ ├── index.tsx # / (Home page)
│ ├── layout.tsx # Root layout
│ ├── hello.md # /hello (Markdown content page)
│ ├── healthz.ts # /healthz handler
│ └── robots.txt.ts # /robots.txt handler
├── components/ # Client Components ("use client")
├── render-document.tsx # HTML document shell (<html>, <head>, <body>)
├── factory.ts # App types & factory helpers
└── index.ts # Hono app — createApp(), route registration
- Create
src/routes/my-page.tsxand export a Server Component - Register both the HTML route and the RSC payload route in
src/index.tsx:
// HTML route
app.get("/my-page", rscMiddleware, (c) =>
c.get("renderPage")(
c.req.raw,
() => import("@/routes/my-page").then((m) => ({ default: m.MyPage })),
false
)
);
// RSC payload route
app.get("/__rsc/my-page", rscMiddleware, (c) =>
c.get("renderPage")(
c.req.raw,
() => import("@/routes/my-page").then((m) => ({ default: m.MyPage })),
true
)
);- Declare the binding in
wrangler.toml:
[[kv_namespaces]]
binding = "MY_KV"
id = "..."- Add the type in
src/bindings.ts:
export interface Env {
MY_KV: KVNamespace;
}- Use it in any Hono route via
c.env:
app.get("/api/data", async (c) => {
const value = await c.env.MY_KV.get("my-key");
return c.json({ value });
});env is passed from the Workers runtime through handler(request, env) → app.fetch(request, env), so all routes have full access.
This repo's commit history shows the evolution:
-
init: naive plugin-rsc + Hono fallback— The simplest working setup.
pagesobject inentry.rsc.tsx, Hono only for unmatched routes. -
refactor: integrate RSC as Hono middleware— Hono handles all routing. RSC rendering viarscMiddleware+renderPagecontext. -
refactor: switch from .rsc suffix to /__rsc/ path prefix— Current design.
RSC requests use/__rsc/path prefix (inspired by Waku's/RSC/). Separate routes for HTML and RSC payloads;isRscis passed explicitly by the route handler.
@vitejs/plugin-rsc is a low-level RSC protocol implementation, not a full RSC framework.
This template covers the basics — Server Components + Streaming SSR — but the following are not implemented:
| Feature | Status | Alternative |
|---|---|---|
Server Actions ("use server") |
❌ Not supported | Waku, Next.js |
Client Components ("use client") |
✅ Works automatically | — |
| File-based routing + auto layout nesting | ❌ Manual registration | Next.js, Waku |
| Cloudflare bindings (KV, D1, R2) | ✅ Available via c.env |
— |
If you need Server Actions or a full RSC feature set, consider:
This template is for those who want RSC rendering + Hono API on Workers, with full control over the stack and minimal abstractions.