TanStack Start
TanStack Start is pure Vite —
tanstackStart() and viteReact() are ordinary plugins in
vite.config.ts, so
Cloudflare.Website.Vite builds the client
assets and the SSR server bundle in a single vite build pass, no
adapter or Wrangler config required. Support is verified: the
examples/cloudflare-tanstack
example is checked in, and a live test deploys it in dev mode with real
Alchemy-managed R2 bindings.
Configure Vite
Section titled “Configure Vite”Your Vite config stays exactly what TanStack Start’s scaffold gives you — Alchemy layers its Cloudflare integration on top when it builds:
import { tanstackStart } from "@tanstack/react-start/plugin/vite";import viteReact from "@vitejs/plugin-react";import { defineConfig } from "vite";
export default defineConfig({ plugins: [tanstackStart(), viteReact()],});Remove @cloudflare/vite-plugin
Section titled “Remove @cloudflare/vite-plugin”Also drop the dependency:
bun remove @cloudflare/vite-pluginnpm uninstall @cloudflare/vite-pluginpnpm remove @cloudflare/vite-pluginyarn remove @cloudflare/vite-pluginDeclare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import Backend, { Bucket } from "./src/backend.ts";
export const Website = Cloudflare.Website.Vite("Website", { env: { BUCKET: Bucket, BACKEND: Backend, },});
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;Each env entry becomes a native Worker binding (BUCKET an
R2 bucket, BACKEND a service binding to another Worker). Requests
matching a built client asset are served directly from the asset layer;
everything else (SSR routes, server routes) falls through to the
TanStack server handler. WebsiteEnv is the typed
shape of those bindings, derived from the declaration. The SSR 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 the Stack alongside anything it binds to:
export default Alchemy.Stack( "CloudflareTanstackExample", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const backend = yield* Backend; const website = yield* Website;
return { backendUrl: backend.url.as<string>(), websiteUrl: website.url.as<string>(), }; }),);One Stack deploys the Backend Worker, the R2 bucket, and the TanStack Start app together, and returns both URLs as stack outputs.
Read bindings in server code
Section titled “Read bindings in server code”Expose the Worker env through a small module so every server route
imports the same typed handle:
import * as cf from "cloudflare:workers";import type { WebsiteEnv } from "../alchemy.run.ts";
// In development mode with TanStack Start, `import { env } from "cloudflare:workers"`// does not work at the top level.// As a workaround, we use a proxy to access the env object.export const env = new Proxy({} as WebsiteEnv, { get(_, prop) { return cf.env[prop as keyof typeof cf.env]; },});The Proxy matters: TanStack Start’s dev server evaluates route modules
outside the Worker request context, so a top-level
import { env } from "cloudflare:workers" breaks in dev — deferring the
property access until a handler actually runs sidesteps it.
Call bindings from a route
Section titled “Call bindings from a route”Inside a server route handler, env is the standard Cloudflare runtime
API, fully typed:
const object = await env.BUCKET.get(key);
const res = await env.BACKEND.fetch( `https://backend/?key=${encodeURIComponent(key)}`,);env.BACKEND is a service binding to an Effect-native Worker — beyond
fetch, you can call its typed RPC methods directly; see
Schemaless RPC.
Deploy and dev
Section titled “Deploy and dev”bun alchemy deployFor local development:
bun alchemy devalchemy dev boots TanStack Start’s Vite dev server with HMR while the
bindings stay attached to the real Alchemy-managed cloud resources —
this exact combination (TanStack Start dev mode + live R2 binding) is
covered by a live test in the Alchemy repo, so env.BUCKET.get(key)
hits the same bucket in dev as in production.
Solid variant
Section titled “Solid variant”TanStack Start’s Solid flavor works the same way — swap the plugins in
vite.config.ts and keep the identical alchemy.run.ts shape (see
examples/cloudflare-tanstack-start-solid):
import { tanstackStart } from "@tanstack/solid-start/plugin/vite";import { defineConfig } from "vite";import viteSolid from "vite-plugin-solid";
export default defineConfig({ plugins: [tanstackStart(), viteSolid({ ssr: true })],});@tanstack/solid-start provides the same tanstackStart() plugin
entry point, so Cloudflare.Website.Vite builds it identically —
no changes needed.