1---
2title: TanStack Start
3description: Deploy TanStack Start (React or Solid) to AWS with AWS.Website.TanStackStart — SSR on a streaming Lambda Function URL, client assets on S3 + CloudFront, 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`AWS.Website.TanStackStart` deploys a
13[TanStack Start](https://tanstack.com/start) app to AWS. TanStack Start
14is pure Vite, so Alchemy runs your project's own `vite build`: the SSR
15server runs on a Lambda Function URL with response streaming, and client
16assets are served from a private S3 bucket through CloudFront. A
17CloudFront Function routes each request at the edge — uploaded files go
18to S3, everything else streams from the Lambda.
19
20## Install
21
22The build integration is not bundled with alchemy. Install
23`@alchemy.run/frontend-frameworks`; the resource loads its
24`/tanstack-start` and `/tanstack-start/aws` exports from your project at
25deploy time. It is only used at build time, so a dev dependency is
26enough:
27
28<Tabs syncKey="pkgManager">
29 <TabItem label="bun" icon="bun">
30 <Code code={`bun add -d ${devDeps}`} lang="sh" />
31 </TabItem>
32 <TabItem label="npm" icon="npm">
33 <Code code={`npm install -D ${devDeps}`} lang="sh" />
34 </TabItem>
35 <TabItem label="pnpm" icon="pnpm">
36 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
37 </TabItem>
38 <TabItem label="yarn" icon="seti:yarn">
39 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
40 </TabItem>
41</Tabs>
42
43## Configure Vite
44
45Your `vite.config.ts` stays exactly what TanStack Start's scaffold gives
46you — no adapter, no deployment preset:
47
48```typescript
49// vite.config.ts
50import { tanstackStart } from "@tanstack/react-start/plugin/vite";
51import viteReact from "@vitejs/plugin-react";
52import { defineConfig } from "vite";
53
54export default defineConfig({
55 plugins: [tanstackStart(), viteReact()],
56});
57```
58
59Alchemy drives that config's `builder.buildApp()` and forces the SSR
60bundle to be self-contained, then wraps its fetch handler as a streaming
61Lambda handler.
62
63:::caution[Remove @cloudflare/vite-plugin if present]
64The TanStack CLI adds `@cloudflare/vite-plugin` when scaffolding a new
65app. It builds for workerd, not Node, so an AWS deploy needs it gone:
66
67```diff lang="typescript"
68// vite.config.ts
69-import { cloudflare } from "@cloudflare/vite-plugin";
70import { tanstackStart } from "@tanstack/react-start/plugin/vite";
71import viteReact from "@vitejs/plugin-react";
72import { defineConfig } from "vite";
73
74export default defineConfig({
75- plugins: [
76- cloudflare({ viteEnvironment: { name: "ssr" } }),
77- tanstackStart(),
78- viteReact(),
79- ],
80+ plugins: [tanstackStart(), viteReact()],
81});
82```
83
84:::
85
86## Declare the Website
87
88Declare the site as a module-level const (rather than inline in the
89Stack):
90
91```typescript
92// alchemy.run.ts
93import * as AWS from "alchemy/AWS";
94
95export const Website = AWS.Website.TanStackStart("Website");
96```
97
98`rootDir` is the project root — the directory holding `package.json`
99and `vite.config.*`. It defaults to `"."`, so set it only when the app
100does not sit next to `alchemy.run.ts`:
101
102```typescript
103export const Website = AWS.Website.TanStackStart("Website", {
104 rootDir: "./app",
105});
106```
107
108## Add it to the Stack
109
110Yield the site from your Stack and return its URL:
111
112```typescript
113// alchemy.run.ts
114import * as Alchemy from "alchemy";
115import * as Effect from "effect/Effect";
116
117export default Alchemy.Stack(
118 "MyTanStackSite",
119 {
120 providers: AWS.providers(),
121 state: AWS.state(),
122 },
123 Effect.gen(function* () {
124 const site = yield* Website;
125 return { url: site.url };
126 }),
127);
128```
129
130Requests matching a built client asset are served straight from S3;
131everything else — SSR routes, server routes, server functions — falls
132through to the Lambda.
133
134## Add environment variables
135
136The server function's environment is configured under `env`:
137
138```diff lang="typescript"
139// alchemy.run.ts
140export const Website = AWS.Website.TanStackStart("Website", {
141+ memorySize: 2048,
142+ env: {
143+ API_BASE: "https://api.example.com",
144+ },
145});
146```
147
148`env` values are set on the Lambda on deploy and injected into
149the dev server's process environment under `alchemy dev`, so server code
150reads the same values in both modes. Sibling props tune the
151Lambda itself (`memorySize`, `timeout`, `architecture`, `runtime`).
152
153An output from another resource only exists once that resource is
154yielded, so declare the site inside the Stack generator instead:
155
156```typescript
157// alchemy.run.ts
158Effect.gen(function* () {
159 const api = yield* Api;
160
161 const site = yield* AWS.Website.TanStackStart("Website", {
162 env: { API_BASE: api.url },
163 });
164
165 return { url: site.url };
166});
167```
168
169## Read the environment in a server function
170
171The TanStack Start server runs in a plain Node Lambda, so server
172functions read the environment from `process.env`:
173
174```typescript
175// src/routes/index.tsx
176import { createServerFn } from "@tanstack/react-start";
177
178const getApiBase = createServerFn({ method: "GET" }).handler(() => ({
179 apiBase: process.env.API_BASE ?? "unset",
180}));
181```
182
183Values prefixed `VITE_` are inlined into the client bundle at build time
184instead, as `import.meta.env.VITE_*` — set those in the shell that runs
185`alchemy deploy`, not in `env`.
186
187## Read the environment in a server route
188
189Server routes run on the same Lambda and read `process.env` the same
190way:
191
192```typescript
193// src/routes/api.hello.ts
194import { createFileRoute } from "@tanstack/react-router";
195
196export const Route = createFileRoute("/api/hello")({
197 server: {
198 handlers: {
199 GET: () => new Response(process.env.API_BASE ?? "unset"),
200 },
201 },
202});
203```
204
205## Call AWS services from server code
206
207The composite creates and owns the server Lambda's execution role, and
208exposes no prop to add permissions to it. Server code that needs to
209reach AWS goes behind its own Lambda, where capabilities are bound
210explicitly:
211
212```typescript
213// alchemy.run.ts
214Effect.gen(function* () {
215 const api = yield* Api;
216
217 const site = yield* AWS.Website.TanStackStart("Website", {
218 env: { API_BASE: api.url },
219 });
220
221 return { url: site.url };
222});
223```
224
225A server function then calls that API over HTTP with the URL it was
226handed:
227
228```typescript
229// src/routes/index.tsx
230import { createServerFn } from "@tanstack/react-start";
231
232const readObject = createServerFn({ method: "GET" }).handler(async () => {
233 const res = await fetch(`${process.env.API_BASE}/hello`);
234 return { body: await res.text() };
235});
236```
237
238See [Effect HTTP API](/aws/apis/effect-http-api) for building that
239Lambda and binding it to the resources it reads.
240
241## Build configuration
242
243The build runs through your project's own `vite.config.ts` — the
244`tanstackStart()` plugin and everything else in it apply as-is. Keep
245vite's default `build.outDir` (`dist`); the integration reads the
246server and client output from `dist/server` and `dist/client`.
247
248## Local dev
249
250```sh
251bun alchemy dev
252```
253
254`alchemy dev` runs TanStack Start's own Vite dev server (native HMR)
255instead of deploying anything; `site.url` is the dev server's
256`http://localhost:<port>` address and no AWS resources are created. Wrap
257the site in `Alchemy.remote()` to deploy the real infrastructure even
258during dev.
259
260## Solid variant
261
262TanStack Start's Solid flavor works the same way — swap the plugins in
263`vite.config.ts` and keep the identical `alchemy.run.ts`:
264
265```typescript
266// vite.config.ts
267import { tanstackStart } from "@tanstack/solid-start/plugin/vite";
268import { defineConfig } from "vite";
269import viteSolid from "vite-plugin-solid";
270
271export default defineConfig({
272 plugins: [tanstackStart(), viteSolid({ ssr: true })],
273});
274```
275
276`@tanstack/solid-start` exposes the same `tanstackStart()` plugin entry
277point and the same `client`/`ssr` build environments, so the deploy is
278identical.
279
280## Custom domain
281
282```typescript
283// alchemy.run.ts
284export const Website = AWS.Website.TanStackStart("Website", {
285 domain: { name: "app.example.com" },
286});
287```
288
289The certificate is created in `us-east-1` (required by CloudFront), DNS
290validation records and the alias record are managed through Route 53.
291The hosted zone is inferred from the hostname — pass `hostedZoneId` to
292pin it when several zones could match.
293
294## Share a distribution with Router
295
296CloudFront distributions take minutes to create. To serve several sites
297(or a site plus an API) from one distribution, attach the site to an
298[`AWS.Website.Router`](/aws/frontend/static-site#compose-sites-with-a-router):
299
300The site takes the Router's outputs, so both are yielded inside the
301Stack generator:
302
303```typescript
304// alchemy.run.ts
305Effect.gen(function* () {
306 const router = yield* AWS.Website.Router("FrontDoor", {
307 domain: { name: "example.com" },
308 });
309
310 const site = yield* AWS.Website.TanStackStart("Website", {
311 domain: { router },
312 });
313
314 return { url: site.url };
315});
316```