Skip to content

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.

Your Vite config stays exactly what TanStack Start’s scaffold gives you — Alchemy layers its Cloudflare integration on top when it builds:

vite.config.ts
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tanstackStart(), viteReact()],
});

Also drop the dependency:

Terminal window
bun remove @cloudflare/vite-plugin

Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:

alchemy.run.ts
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.

Yield the Website from the Stack alongside anything it binds to:

alchemy.run.ts
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.

Expose the Worker env through a small module so every server route imports the same typed handle:

src/env.ts
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.

Inside a server route handler, env is the standard Cloudflare runtime API, fully typed:

src/routes/api.hello.ts
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.

Terminal window
bun alchemy deploy

For local development:

Terminal window
bun alchemy dev

alchemy 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.

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):

vite.config.ts
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.