Add a Vite SPA
Cloudflare.Vite invokes Vite programmatically (via
createBuilder), builds your client assets, ships them to
Cloudflare, and serves them through a Worker — so your frontend
and backend share one URL surface and one deploy.
Pick the path that fits where you’re starting from:
- Set it up manually — start from an
empty project; the bare minimum is an
index.htmland an entry module. - Use the
create-vitetemplate — start from Vite’s official React + TS scaffold. - Deploy an existing Vite project — point Alchemy at a SPA you already have.
Set it up manually
Section titled “Set it up manually”The bare minimum is an index.html with a script tag. No
vite.config.ts, no package.json build script, no main
entry. Alchemy supplies the Vite config itself.
A minimal React SPA looks like:
.├── index.html # entry HTML, references the client bundle└── src/ └── main.tsx # client entry imported by index.htmlInstall React
Section titled “Install React”bun add react react-dombun add -d @types/react @types/react-dom @vitejs/plugin-reactCreate index.html
Section titled “Create index.html”index.html just needs to load your entry module:
<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <title>My App</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body></html>Create src/main.tsx
Section titled “Create src/main.tsx”Mount a React component into the #root div:
import React from "react";import ReactDOM from "react-dom/client";
function App() { return ( <main> <h1>Hello from Alchemy + Vite</h1> <p>Edit <code>src/main.tsx</code> and redeploy.</p> </main> );}
ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <App /> </React.StrictMode>,);Add it to the Stack
Section titled “Add it to the Stack”Yield Cloudflare.Vite("Website") from your Stack:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import Worker from "./src/worker.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const worker = yield* Worker; const web = yield* Cloudflare.Vite("Website"); return { url: worker.url, webUrl: web.url, }; }),);The defaults set notFoundHandling: "single-page-application",
which is what makes client-side routing work — a deep link like
/about returns index.html instead of a 404, and your router
handles it on the client.
Any framework Vite supports works the same way — vanilla TS,
Vue, Solid, Svelte — bring whatever src/ layout the framework
wants.
Use the create-vite template
Section titled “Use the create-vite template”If you’d rather start from a real framework scaffold (React + TS with HMR, ESLint, etc.), use Vite’s official template:
bun create vite@latest web -- --template react-tscd web && bun install && cd ..That drops a complete project into ./web/ with its own
package.json, tsconfig.json, and vite.config.ts.
Point Cloudflare.Vite at the subfolder
Section titled “Point Cloudflare.Vite at the subfolder”Since the SPA isn’t at the project root, set rootDir:
Effect.gen(function* () { const worker = yield* Worker; const web = yield* Cloudflare.Vite("Website", { rootDir: "./web", }); return { url: worker.url, webUrl: web.url, };}),rootDir defaults to process.cwd(), so you only set it when
your index.html isn’t next to alchemy.run.ts.
Deploy an existing Vite project
Section titled “Deploy an existing Vite project”Already have a Vite SPA? Point Cloudflare.Vite at it with
rootDir and you’re done:
const web = yield* Cloudflare.Vite("Website", { rootDir: "./path/to/your/spa",});Your existing vite.config.ts, plugins, aliases, and tsconfig
are all preserved — Alchemy merges its Cloudflare integration on
top of your config. Two things to check first:
Deploy
Section titled “Deploy”bun alchemy deployAlchemy runs Vite on rootDir, uploads the assets, creates a
Worker that serves them, and prints the new webUrl stack output:
{ url: "https://myapp-worker-dev-you.workers.dev", webUrl: "https://myapp-web-dev-you.workers.dev",}Verify
Section titled “Verify”Hit it with curl:
curl -s https://myapp-web-dev-you.workers.dev | head -5# <!doctype html># <html lang="en"># <head># <meta charset="UTF-8" /># ...Or add an integration test alongside the existing ones:
import * as Cloudflare from "alchemy/Cloudflare";import * as Test from "alchemy/Test/Bun";import { expect } from "bun:test";import * as Effect from "effect/Effect";import * as HttpClient from "effect/unstable/http/HttpClient";import Stack from "../alchemy.run.ts";
const { test, beforeAll, deploy } = Test.make({ providers: Cloudflare.providers(), state: Cloudflare.state(),});
const stack = beforeAll(deploy(Stack));
test( "Web SPA serves index.html", Effect.gen(function* () { const { webUrl } = yield* stack;
const response = yield* HttpClient.get(webUrl); expect(response.status).toBe(200); expect(yield* response.text).toContain("<!doctype html>"); }),);Local dev
Section titled “Local dev”Coming soon.
Use a different framework
Section titled “Use a different framework”Cloudflare.Vite works with any Vite-based framework. The setup
above is the SPA case (no SSR). For SSR frameworks, drop the
assets.config block and let the framework’s Vite plugin own the
entry. Common choices:
- TanStack Start — full-stack React with file-based routing and server functions. See examples/cloudflare-tanstack.
- SolidStart — SSR Solid with file-based routing. See examples/cloudflare-solidstart.
- SolidJS SSR (manual) — when you want full control over the SSR pipeline rather than SolidStart’s conventions. See examples/cloudflare-solidjs-ssr.
- Vue 3 SPA — Vite’s default Vue template, with the same
notFoundHandling: "single-page-application"config as above. See examples/cloudflare-vue. - Plain static site — a single
index.html(no framework) ships throughCloudflare.Vite("Website")with no extra config. See examples/cloudflare-static-site.
For deeper coverage of framework-specific patterns (asset configs, SSR vs SPA tradeoffs, custom build steps), see the Frontend frameworks guide.
Your app now ships a Worker backend and a Vite frontend from the
same alchemy.run.ts, deploying together with one command. Next
you’ll run a Container so each
Durable Object instance has its own long-lived process for
executing untrusted code or running binaries.