1---
2title: TanStack Start
3description: Deploy TanStack Start (React or Solid) to Cloudflare with Cloudflare.Website.Vite — SSR, typed Worker bindings, and HMR dev with real cloud resources.
4sidebar:
5 order: 5
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10[TanStack Start](https://tanstack.com/start) is pure Vite —
11`tanstackStart()` and `viteReact()` are ordinary plugins in
12`vite.config.ts`, so
13[`Cloudflare.Website.Vite`](/cloudflare/frontend/vite) builds the client
14assets and the SSR server bundle in a single `vite build` pass, no
15adapter or Wrangler config required. Support is verified: the
16[examples/cloudflare-website-tanstack-start](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-tanstack-start)
17example is checked in, and a live test deploys it in dev mode with real
18Alchemy-managed R2 bindings.
19
20## Configure Vite
21
22Your Vite config stays exactly what TanStack Start's scaffold gives
23you — Alchemy layers its Cloudflare integration on top when it
24builds:
25
26```typescript
27// vite.config.ts
28import { tanstackStart } from "@tanstack/react-start/plugin/vite";
29import viteReact from "@vitejs/plugin-react";
30import { defineConfig } from "vite";
31
32export default defineConfig({
33 plugins: [tanstackStart(), viteReact()],
34});
35```
36
37## Remove `@cloudflare/vite-plugin`
38
39:::caution[Remove @cloudflare/vite-plugin if present]
40The TanStack CLI adds `@cloudflare/vite-plugin` to `vite.config.ts`
41automatically when scaffolding a new app. Alchemy appends its own
42Cloudflare Vite plugin and is **not compatible** with
43`@cloudflare/vite-plugin` — remove it:
44
45```diff lang="typescript"
46// vite.config.ts
47import { tanstackStart } from "@tanstack/react-start/plugin/vite";
48import viteReact from "@vitejs/plugin-react";
49import { defineConfig } from "vite";
50-import { cloudflare } from "@cloudflare/vite-plugin";
51
52export default defineConfig({
53- plugins: [
54- cloudflare({ viteEnvironment: { name: "ssr" } }),
55- tanstackStart(),
56- viteReact(),
57- ],
58+ plugins: [tanstackStart(), viteReact()],
59});
60```
61
62:::
63
64Also drop the dependency:
65
66<Tabs syncKey="pkgManager">
67 <TabItem label="bun" icon="bun">
68 <Code code="bun remove @cloudflare/vite-plugin" lang="sh" />
69 </TabItem>
70 <TabItem label="npm" icon="npm">
71 <Code code="npm uninstall @cloudflare/vite-plugin" lang="sh" />
72 </TabItem>
73 <TabItem label="pnpm" icon="pnpm">
74 <Code code="pnpm remove @cloudflare/vite-plugin" lang="sh" />
75 </TabItem>
76 <TabItem label="yarn" icon="seti:yarn">
77 <Code code="yarn remove @cloudflare/vite-plugin" lang="sh" />
78 </TabItem>
79</Tabs>
80
81## Declare the Website
82
83Declare the site as a module-level const (rather than inline in the
84Stack) and derive the typed shape of its bindings from it:
85
86```typescript
87// alchemy.run.ts
88import * as Alchemy from "alchemy";
89import * as Cloudflare from "alchemy/Cloudflare";
90import * as Effect from "effect/Effect";
91import Backend, { Bucket } from "./src/backend.ts";
92
93export const Website = Cloudflare.Website.Vite("Website", {
94 env: {
95 BUCKET: Bucket,
96 BACKEND: Backend,
97 },
98});
99
100export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
101```
102
103Each `env` entry becomes a native Worker binding (`BUCKET` an
104R2 bucket, `BACKEND` a service binding to another Worker). Requests
105matching a built client asset are served directly from the asset layer;
106everything else (SSR routes, server routes) falls through to the
107TanStack server handler. `WebsiteEnv` is the typed
108shape of those bindings, derived from the declaration. The SSR server
109bundle's Node APIs are covered by the `nodejs_compat` compatibility
110flag, which is enabled by default for every Worker.
111
112## Add it to the Stack
113
114Yield the Website from the Stack alongside anything it binds to:
115
116```typescript
117// alchemy.run.ts
118export default Alchemy.Stack(
119 "CloudflareTanstackExample",
120 {
121 providers: Cloudflare.providers(),
122 state: Cloudflare.state(),
123 },
124 Effect.gen(function* () {
125 const backend = yield* Backend;
126 const website = yield* Website;
127
128 return {
129 backendUrl: backend.url.as<string>(),
130 websiteUrl: website.url.as<string>(),
131 };
132 }),
133);
134```
135
136One Stack deploys the Backend Worker, the R2 bucket, and the TanStack
137Start app together, and returns both URLs as stack outputs.
138
139## Read bindings in server code
140
141Expose the Worker `env` through a small module so every server route
142imports the same typed handle:
143
144```typescript
145// src/env.ts
146import * as cf from "cloudflare:workers";
147import type { WebsiteEnv } from "../alchemy.run.ts";
148
149// In development mode with TanStack Start, `import { env } from "cloudflare:workers"`
150// does not work at the top level.
151// As a workaround, we use a proxy to access the env object.
152export const env = new Proxy({} as WebsiteEnv, {
153 get(_, prop) {
154 return cf.env[prop as keyof typeof cf.env];
155 },
156});
157```
158
159The Proxy matters: TanStack Start's dev server evaluates route modules
160outside the Worker request context, so a top-level
161`import { env } from "cloudflare:workers"` breaks in dev — deferring the
162property access until a handler actually runs sidesteps it.
163
164## Call bindings from a route
165
166Inside a server route handler, `env` is the standard Cloudflare runtime
167API, fully typed:
168
169```typescript
170// src/routes/api.hello.ts
171const object = await env.BUCKET.get(key);
172
173const res = await env.BACKEND.fetch(
174 `https://backend/?key=${encodeURIComponent(key)}`,
175);
176```
177
178`env.BACKEND` is a service binding to an Effect-native Worker — beyond
179`fetch`, you can call its typed RPC methods directly; see
180[Schemaless RPC](/cloudflare/apis/schemaless-rpc).
181
182## Deploy and dev
183
184```sh
185bun alchemy deploy
186```
187
188For local development:
189
190```sh
191bun alchemy dev
192```
193
194`alchemy dev` boots TanStack Start's Vite dev server with HMR while the
195bindings stay attached to the **real** Alchemy-managed cloud resources —
196this exact combination (TanStack Start dev mode + live R2 binding) is
197covered by a live test in the Alchemy repo, so `env.BUCKET.get(key)`
198hits the same bucket in dev as in production.
199
200## Solid variant
201
202TanStack Start's Solid flavor works the same way — swap the plugins in
203`vite.config.ts` and keep the identical `alchemy.run.ts` shape (see
204[examples/cloudflare-tanstack-start-solid](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-tanstack-start-solid)):
205
206```typescript
207// vite.config.ts
208import { tanstackStart } from "@tanstack/solid-start/plugin/vite";
209import { defineConfig } from "vite";
210import viteSolid from "vite-plugin-solid";
211
212export default defineConfig({
213 plugins: [tanstackStart(), viteSolid({ ssr: true })],
214});
215```
216
217`@tanstack/solid-start` provides the same `tanstackStart()` plugin
218entry point, so `Cloudflare.Website.Vite` builds it identically —
219no changes needed.