1---
2title: TanStack Start
3description: Deploy TanStack Start (React or Solid) to Railway with Railway.Website.TanStackStart — SSR on a container Service, client assets in 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`Railway.Website.TanStackStart` deploys a
13[TanStack Start](https://tanstack.com/start) app to Railway. TanStack
14Start is pure Vite, so Alchemy runs your project's own `vite build`:
15the SSR server plus client assets run on one
16[`Railway.Service`](/railway/compute/services). Alchemy creates a
17[`Railway.Project`](/railway/compute/projects) if you omit
18`project`. The URL is the generated `*.up.railway.app` hostname. No
19adapter.
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 Railway deploy needs it
69gone:
70
71```diff lang="typescript"
72// vite.config.ts
73-import { cloudflare } from "@cloudflare/vite-plugin";
74import { tanstackStart } from "@tanstack/react-start/plugin/vite";
75import viteReact from "@vitejs/plugin-react";
76import { defineConfig } from "vite";
77
78export default defineConfig({
79- plugins: [
80- cloudflare({ viteEnvironment: { name: "ssr" } }),
81- tanstackStart(),
82- viteReact(),
83- ],
84+ plugins: [tanstackStart(), viteReact()],
85});
86```
87
88:::
89
90## Declare the Website
91
92Declare the site as a module-level const (rather than inline in the
93Stack):
94
95```typescript
96// alchemy.run.ts
97import * as Railway from "alchemy/Railway";
98
99export const Website = Railway.Website.TanStackStart("Website");
100```
101
102Pass `project` to put the Service in an existing `Railway.Project`.
103`rootDir` is the project root — the directory holding `package.json`
104and `vite.config.*`. It defaults to `"."`, so set it only when the app
105does not sit next to `alchemy.run.ts`:
106
107```typescript
108export const Website = Railway.Website.TanStackStart("Website", {
109 rootDir: "./app",
110});
111```
112
113## Add it to the Stack
114
115Yield the site from your Stack and return its URL:
116
117```typescript
118// alchemy.run.ts
119import * as Alchemy from "alchemy";
120import * as Effect from "effect/Effect";
121
122export default Alchemy.Stack(
123 "MyTanStackSite",
124 {
125 providers: Railway.providers(),
126 state: Alchemy.localState(),
127 },
128 Effect.gen(function* () {
129 const site = yield* Website;
130 return { url: site.url };
131 }),
132);
133```
134
135Requests matching a built client asset are served first; everything
136else — SSR routes, server routes, server functions — falls through to
137the Node handler on the Service.
138
139The live `url` is `https://{name}.up.railway.app`. `site.service`
140and `site.project` are the Railway resources underneath —
141`undefined` during `alchemy dev`.
142
143See the
144[TanStack Start API reference](/providers/railway/website/tanstackstart)
145for every prop and attribute.
146
147## Add environment variables
148
149Process environment is a top-level `env` map — plain strings, or
150`Redacted` values:
151
152```diff lang="typescript"
153// alchemy.run.ts
154export const Website = Railway.Website.TanStackStart("Website", {
155+ env: {
156+ API_BASE: "https://api.example.com",
157+ },
158});
159```
160
161The values are copied onto `process.env` before the build and the
162dev server, and onto the Service at deploy time, so server code
163reads the same keys in both modes. Client inlining is whatever Vite
164does (`VITE_` prefixes).
165
166An output from another resource only exists once that resource is
167yielded, so declare the site inside the Stack generator instead:
168
169```typescript
170// alchemy.run.ts
171Effect.gen(function* () {
172 const api = yield* Api;
173
174 const site = yield* Railway.Website.TanStackStart("Website", {
175 env: { API_BASE: api.url },
176 });
177
178 return { url: site.url };
179});
180```
181
182## Read the environment in a server function
183
184The TanStack Start server is a plain Node process, so server
185functions read the environment from `process.env`:
186
187```typescript
188// src/routes/index.tsx
189import { createServerFn } from "@tanstack/react-start";
190
191const getApiBase = createServerFn({ method: "GET" }).handler(() => ({
192 apiBase: process.env.API_BASE ?? "unset",
193}));
194```
195
196Values prefixed `VITE_` are inlined into the client bundle at build time
197instead, as `import.meta.env.VITE_*` — set those in the shell that runs
198`alchemy deploy`, not in `env`.
199
200## Read the environment in a server route
201
202Server routes run on the same Node process and read `process.env` the
203same way:
204
205```typescript
206// src/routes/api.hello.ts
207import { createFileRoute } from "@tanstack/react-router";
208
209export const Route = createFileRoute("/api/hello")({
210 server: {
211 handlers: {
212 GET: () => new Response(process.env.API_BASE ?? "unset"),
213 },
214 },
215});
216```
217
218## Build configuration
219
220The build runs through your project's own `vite.config.ts` — the
221`tanstackStart()` plugin and everything else in it apply as-is. Keep
222vite's default `build.outDir` (`dist`); the integration reads the
223server and client output from `dist/server` and `dist/client`.
224
225## Local dev
226
227```sh
228bun alchemy dev
229```
230
231`alchemy dev` runs TanStack Start's own Vite dev server (native HMR)
232instead of deploying; `site.url` is the local address and no Railway
233resources are created. Wrap the site in
234`Alchemy.remote()` to deploy the live Service even during dev:
235
236```typescript
237export const Website = Railway.Website.TanStackStart("Website").pipe(
238 Alchemy.remote(),
239);
240```
241
242## Solid variant
243
244TanStack Start's Solid flavor works the same way — swap the plugins in
245`vite.config.ts` and keep the identical `alchemy.run.ts`:
246
247```typescript
248// vite.config.ts
249import { tanstackStart } from "@tanstack/solid-start/plugin/vite";
250import { defineConfig } from "vite";
251import viteSolid from "vite-plugin-solid";
252
253export default defineConfig({
254 plugins: [tanstackStart(), viteSolid({ ssr: true })],
255});
256```
257
258`@tanstack/solid-start` exposes the same `tanstackStart()` plugin entry
259point and the same `client`/`ssr` build environments, so the deploy is
260identical.
261
262## Custom domain
263
264```typescript
265const site = yield* Railway.Website.TanStackStart("Web", {
266 domain: "app.example.com",
267});
268```
269
270`domain` attaches a [`Railway.CustomDomain`](/railway/networking)
271(`targetPort` 3000). `url` becomes `https://app.example.com`
272instead of the generated `*.up.railway.app`.
273
274## Where next
275
276- [TanStack Start API reference](/providers/railway/website/tanstackstart)
277- [Projects](/railway/compute/projects) and
278 [Services](/railway/compute/services)
279- [Custom domains](/railway/networking)