1---
2title: TanStack Start
3description: Deploy TanStack Start (React or Solid) to Fly with Fly.Website.TanStackStart — SSR on a Machine, client assets baked into the image, and TanStack Start's own Vite dev server under alchemy dev.
4sidebar:
5 order: 8
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Fly.Website.TanStackStart` deploys a
13[TanStack Start](https://tanstack.com/start) app to Fly. TanStack Start
14is pure Vite, so Alchemy runs your project's own `vite build`: the SSR
15server runs as a Node [`Fly.Service`](/fly/compute/services) on a
16Machine, and client assets are baked into the image and served first.
17Omit `app` and Alchemy creates a
18[`Fly.App`](/fly/compute/apps) plus a shared IPv4 so
19`https://{app}.fly.dev` answers. No adapter, no `fly.toml`.
20
21## Install
22
23The build integration is not bundled with alchemy. Install
24`@alchemy.run/frontend-frameworks`; the resource loads its
25`/tanstack-start` and `/tanstack-start/node` exports from your project
26at deploy time. It is only used at build time, so a dev dependency is
27enough:
28
29<Tabs syncKey="pkgManager">
30 <TabItem label="bun" icon="bun">
31 <Code code={`bun add -d ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="npm" icon="npm">
34 <Code code={`npm install -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="pnpm" icon="pnpm">
37 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39 <TabItem label="yarn" icon="seti:yarn">
40 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
41 </TabItem>
42</Tabs>
43
44## Configure Vite
45
46Your `vite.config.ts` stays what TanStack Start's scaffold gives
47you — no adapter, no deployment preset. Vite plugins like Tailwind
48go in `plugins` as usual:
49
50```typescript
51// vite.config.ts
52import tailwindcss from "@tailwindcss/vite";
53import { tanstackStart } from "@tanstack/react-start/plugin/vite";
54import viteReact from "@vitejs/plugin-react";
55import { defineConfig } from "vite";
56
57export default defineConfig({
58 plugins: [tailwindcss(), tanstackStart(), viteReact()],
59});
60```
61
62Alchemy drives that config's `builder.buildApp()` and forces the SSR
63bundle to be self-contained, then wraps its fetch handler as a Node
64HTTP server on port 3000.
65
66:::caution[Remove @cloudflare/vite-plugin if present]
67The TanStack CLI adds `@cloudflare/vite-plugin` when scaffolding a new
68app. It builds for workerd, not Node, so a Fly deploy needs it gone:
69
70```diff lang="typescript"
71// vite.config.ts
72-import { cloudflare } from "@cloudflare/vite-plugin";
73import { tanstackStart } from "@tanstack/react-start/plugin/vite";
74import viteReact from "@vitejs/plugin-react";
75import { defineConfig } from "vite";
76
77export default defineConfig({
78- plugins: [
79- cloudflare({ viteEnvironment: { name: "ssr" } }),
80- tanstackStart(),
81- viteReact(),
82- ],
83+ plugins: [tanstackStart(), viteReact()],
84});
85```
86
87:::
88
89## Declare the Website
90
91Declare the site as a module-level const (rather than inline in the
92Stack):
93
94```typescript
95// alchemy.run.ts
96import * as Fly from "alchemy/Fly";
97
98export const Website = Fly.Website.TanStackStart("Website");
99```
100
101Pass `app` to put the site on an existing `Fly.App`. `rootDir` is the
102project root — the directory holding `package.json` and `vite.config.*`.
103It defaults to `"."`, so set it only when the app does not sit next to
104`alchemy.run.ts`:
105
106```typescript
107export const Website = Fly.Website.TanStackStart("Website", {
108 rootDir: "./app",
109});
110```
111
112## Add it to the Stack
113
114Yield the site from your Stack and return its URL:
115
116```typescript
117// alchemy.run.ts
118import * as Alchemy from "alchemy";
119import * as Effect from "effect/Effect";
120
121export default Alchemy.Stack(
122 "MyTanStackSite",
123 {
124 providers: Fly.providers(),
125 state: Alchemy.localState(),
126 },
127 Effect.gen(function* () {
128 const site = yield* Website;
129 return { url: site.url };
130 }),
131);
132```
133
134Requests matching a built client asset are served first; everything
135else — SSR routes, server routes, server functions — falls through to
136the Node handler on the Machine.
137
138The live `url` is `https://{app}.fly.dev`. `site.app`,
139`site.service`, `site.ip`, and `site.certificate` are the Fly
140resources underneath — `undefined` during `alchemy dev`.
141
142See the
143[TanStack Start API reference](/providers/fly/website/tanstackstart)
144for every prop and attribute.
145
146## Add environment variables
147
148Process environment is a top-level `env` map — plain strings, or
149`Redacted` values:
150
151```diff lang="typescript"
152// alchemy.run.ts
153export const Website = Fly.Website.TanStackStart("Website", {
154+ env: {
155+ API_BASE: "https://api.example.com",
156+ },
157});
158```
159
160The values are copied onto `process.env` before the build and the
161dev server, and onto the Machine at deploy time, so server code
162reads the same keys in both modes. Client inlining is whatever Vite
163does (`VITE_` prefixes).
164
165An output from another resource only exists once that resource is
166yielded, so declare the site inside the Stack generator instead:
167
168```typescript
169// alchemy.run.ts
170Effect.gen(function* () {
171 const api = yield* Api;
172
173 const site = yield* Fly.Website.TanStackStart("Website", {
174 env: { API_BASE: api.url },
175 });
176
177 return { url: site.url };
178});
179```
180
181## Read the environment in a server function
182
183The TanStack Start server is a plain Node process, so server
184functions read the environment from `process.env`:
185
186```typescript
187// src/routes/index.tsx
188import { createServerFn } from "@tanstack/react-start";
189
190const getApiBase = createServerFn({ method: "GET" }).handler(() => ({
191 apiBase: process.env.API_BASE ?? "unset",
192}));
193```
194
195Values prefixed `VITE_` are inlined into the client bundle at build time
196instead, as `import.meta.env.VITE_*` — set those in the shell that runs
197`alchemy deploy`, not in `env`.
198
199## Read the environment in a server route
200
201Server routes run on the same Node process and read `process.env` the
202same way:
203
204```typescript
205// src/routes/api.hello.ts
206import { createFileRoute } from "@tanstack/react-router";
207
208export const Route = createFileRoute("/api/hello")({
209 server: {
210 handlers: {
211 GET: () => new Response(process.env.API_BASE ?? "unset"),
212 },
213 },
214});
215```
216
217## Build configuration
218
219The build runs through your project's own `vite.config.ts` — the
220`tanstackStart()` plugin and everything else in it apply as-is. Keep
221vite's default `build.outDir` (`dist`); the integration reads the
222server and client output from `dist/server` and `dist/client`.
223
224## Local dev
225
226```sh
227bun alchemy dev
228```
229
230`alchemy dev` runs TanStack Start's own Vite dev server (native HMR)
231instead of deploying; `site.url` is the local address and no Fly
232resources are created. Wrap the site in `Alchemy.remote()` to deploy
233the live App and Service even during dev:
234
235```typescript
236export const Website = Fly.Website.TanStackStart("Website").pipe(
237 Alchemy.remote(),
238);
239```
240
241## Solid variant
242
243TanStack Start's Solid flavor works the same way — swap the plugins in
244`vite.config.ts` and keep the identical `alchemy.run.ts`:
245
246```typescript
247// vite.config.ts
248import { tanstackStart } from "@tanstack/solid-start/plugin/vite";
249import { defineConfig } from "vite";
250import viteSolid from "vite-plugin-solid";
251
252export default defineConfig({
253 plugins: [tanstackStart(), viteSolid({ ssr: true })],
254});
255```
256
257`@tanstack/solid-start` exposes the same `tanstackStart()` plugin entry
258point and the same `client`/`ssr` build environments, so the deploy is
259identical.
260
261## Custom domain
262
263```typescript
264const site = yield* Fly.Website.TanStackStart("Web", {
265 domain: "app.example.com",
266});
267```
268
269`domain` requests ACME (`Fly.Certificate`) on the App. `url`
270becomes `https://app.example.com`. Point DNS at the App first — v1
271does not create DNS records.
272
273## Where next
274
275- [TanStack Start API reference](/providers/fly/website/tanstackstart)
276- [Apps](/fly/compute/apps) and [Services](/fly/compute/services)
277- [IPs & certificates](/fly/networking)