1---
2title: Vite
3description: Deploy a plain Vite app to AWS with AWS.Website.Vite — the vite build output on S3 behind CloudFront, SPA fallback at the edge, and Vite's own dev server under alchemy dev.
4sidebar:
5 order: 2
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`AWS.Website.Vite` deploys a client-only [Vite](https://vite.dev)
13project to AWS. It runs your project's own `vite build` — your
14`vite.config.*` loads natively, plugins included — and serves the
15output from a private S3 bucket through CloudFront. The site is
16assets-only: no Lambda is created, no CloudFormation to write, no
17build command to wire up.
18
19## Install
20
21The build integration is not bundled with alchemy. Install
22`@alchemy.run/frontend-frameworks`; the resource loads its `/vite` and
23`/vite/aws` exports from your project at deploy time. It is only used
24at build time, so a dev dependency is enough:
25
26<Tabs syncKey="pkgManager">
27 <TabItem label="bun" icon="bun">
28 <Code code={`bun add -d ${devDeps}`} lang="sh" />
29 </TabItem>
30 <TabItem label="npm" icon="npm">
31 <Code code={`npm install -D ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="pnpm" icon="pnpm">
34 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="yarn" icon="seti:yarn">
37 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39</Tabs>
40
41## Configure Vite
42
43There is nothing to configure. Your `vite.config.*` loads natively —
44plugins, aliases, and any other non-serializable options work exactly
45as they do outside Alchemy:
46
47```typescript
48// vite.config.ts
49import { defineConfig } from "vite";
50import react from "@vitejs/plugin-react";
51
52export default defineConfig({
53 plugins: [react()],
54});
55```
56
57A project with no config file at all works too: an `index.html` next
58to your `alchemy.run.ts` is enough.
59
60## Declare the Website
61
62Declare the site as a module-level const (rather than inline in the
63Stack):
64
65```typescript
66// alchemy.run.ts
67import * as AWS from "alchemy/AWS";
68
69export const Website = AWS.Website.Vite("Website");
70```
71
72## Add it to the Stack
73
74Yield the site from your Stack and return its URL:
75
76```typescript
77// alchemy.run.ts
78import * as Alchemy from "alchemy";
79import * as Effect from "effect/Effect";
80
81export default Alchemy.Stack(
82 "MyViteSite",
83 {
84 providers: AWS.providers(),
85 state: AWS.state(),
86 },
87 Effect.gen(function* () {
88 const site = yield* Website;
89 return { url: site.url };
90 }),
91);
92```
93
94Deploying uploads the build output to S3, writes the file manifest to
95a CloudFront KeyValueStore, and creates the distribution that serves
96it at the edge.
97
98```sh
99bun alchemy deploy
100```
101
102See
103[examples/aws-website-vite](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-website-vite)
104for the checked-in example.
105
106## Point at a subdirectory
107
108`rootDir` is the Vite project root — the directory holding
109`package.json`, `vite.config.*`, and `index.html`. It defaults to the
110directory you run `alchemy` from:
111
112```typescript
113export const Website = AWS.Website.Vite("Website", {
114 rootDir: "./web",
115});
116```
117
118Vite resolves `index.html` relative to its root, so `index.html` must
119live directly under `rootDir`. See Vite's
120[project structure docs](https://vite.dev/guide/#index-html-and-project-root).
121
122## Deploy-time overrides
123
124Your `vite.config.*` is the home for Vite configuration. The `vite`
125bag is for the exception: a value decided by the deployment rather
126than the project — config files can't consume alchemy `Output`s.
127Overrides merge *over* the config file, so the deploy-time value wins:
128
129```typescript
130export const Website = AWS.Website.Vite("Docs", {
131 vite: {
132 base: "/docs/",
133 outDir: "build",
134 },
135});
136```
137
138`base` is the public path the site deploys under; `outDir` is the
139build output directory, relative to `rootDir`. Leave the bag unset and
140the resolved values from your `vite.config.*` are used — Vite's own
141`outDir` default is `dist`. `base` also applies to the dev server
142under `alchemy dev`.
143
144To load a different config file entirely, pass `config` (relative to
145`rootDir`):
146
147```typescript
148export const Website = AWS.Website.Vite("Web", {
149 config: "vite.deploy.config.ts",
150});
151```
152
153## SPA fallback and 404 pages
154
155A miss — a request that matches no uploaded file — can be answered two
156ways, and they are mutually exclusive. Plain Vite apps are usually
157single-page apps, so `spa` defaults to `true`: misses serve the index
158page with a `200` and the client-side router takes over.
159
160For a multi-page project (an `index.html` per route), turn it off and
161point at the built error page instead:
162
163```diff lang="typescript"
164export const Website = AWS.Website.Vite("Docs", {
165+ spa: false,
166+ errorPage: "404.html",
167});
168```
169
170`errorPage` serves that file with a real `404` status. Pretty URLs
171work in both modes: `/about` serves `about.html` or
172`about/index.html` when one was uploaded, resolved at the edge from
173the file manifest.
174
175## Environment variables
176
177There is no server, so everything the app reads is inlined into the
178bundle at build time — plain Vite semantics. Put `VITE_`-prefixed keys
179in a `.env` file at `rootDir` and read them from `import.meta.env`:
180
181```sh
182# .env
183VITE_API_URL=https://api.example.com
184```
185
186```typescript
187// src/api.ts
188const apiUrl = import.meta.env.VITE_API_URL;
189```
190
191Only `VITE_`-prefixed keys reach the bundle. A committed `.env` is
192hashed with the rest of the project, so editing one re-runs the build
193on the next deploy.
194
195The resource takes no `env` prop, so a value that only exists
196after another resource deploys — an API's Function URL, a bucket name
197— cannot reach this build. Deploy those apps with
198[`AWS.Website.StaticSite`](/aws/frontend/static-site) instead, which
199runs the build as a command and passes `environment` to it.
200
201## Rebuilds are content-hashed
202
203Every non-gitignored file under `rootDir` plus the nearest lockfile is
204content-hashed. If nothing changed, both the build and the deploy are
205skipped. Narrow the scope with `memo` when the project holds large
206directories that don't affect the build output:
207
208```typescript
209export const Website = AWS.Website.Vite("Docs", {
210 memo: {
211 include: ["src/**", "index.html", "package.json"],
212 lockfile: true,
213 },
214});
215```
216
217Setting `include` flips the lockfile default to `false`, so pair it
218with `lockfile: true` to keep rebuilding when dependencies change.
219`memo: false` rebuilds on every deploy. See
220[Memoization](/command/memoization) for the details.
221
222## Cache invalidation on deploy
223
224Every deploy that changes your files invalidates `/*` and moves on
225without waiting. Tune it with `invalidation`:
226
227```typescript
228export const Website = AWS.Website.Vite("Website", {
229 invalidation: {
230 paths: "versioned",
231 wait: true,
232 },
233});
234```
235
236`paths: "versioned"` invalidates only the index page, for builds whose
237assets are content-hashed and immutable; `invalidation: false` skips
238it entirely.
239
240## Local dev
241
242```sh
243bun alchemy dev
244```
245
246`alchemy dev` runs Vite's own dev server (native HMR) instead of
247deploying; `site.url` is the local address and no AWS resources are
248created. Wrap the site in `Alchemy.remote()` to deploy the real
249infrastructure even during dev.
250
251The dev server picks an ephemeral port by default. Pin it to keep the
252local URL stable across runs:
253
254```typescript
255export const Website = AWS.Website.Vite("Website", {
256 dev: { port: 3000 },
257});
258```
259
260## Custom domain
261
262```typescript
263// alchemy.run.ts
264export const Website = AWS.Website.Vite("Website", {
265 domain: { name: "app.example.com" },
266});
267```
268
269The ACM certificate and Route 53 records are managed for you — the
270hosted zone is inferred from the hostname, and `hostedZoneId` pins it
271when several zones could match. A bare string is shorthand for
272`{ name }`.
273
274## Share a CloudFront Router
275
276Attach the site to an existing
277[`AWS.Website.Router`](/aws/frontend/static-site#compose-sites-with-a-router)
278to serve it through one shared distribution instead of its own:
279
280The site takes the Router's *outputs*, so both are yielded inside the
281Stack generator:
282
283```typescript
284Effect.gen(function* () {
285 const router = yield* AWS.Website.Router("Router", {
286 domain: { name: "example.com" },
287 });
288
289 const site = yield* AWS.Website.Vite("Docs", {
290 domain: { router, path: "/docs" },
291 });
292
293 return { url: site.url };
294});
295```
296
297## Frameworks
298
299Anything whose whole deployable output is a `vite build` deploys with
300this resource. Each client-only stack has its own page building on it:
301
302- [React SPA](/aws/frontend/vite-spa) — from a bare `index.html` to
303 the `create-vite` template.
304- [Vue](/aws/frontend/vue) — Vue 3 SPAs with `vue-router` deep links.
305- [Foldkit](/aws/frontend/foldkit) — builds through the same Vite
306 pipeline under its own `AWS.Website.Foldkit` resource.
307
308## SSR frameworks
309
310`AWS.Website.Vite` is static-only — it never creates a server
311function. Frameworks that wrap Vite with a server half deploy through
312their own resources, which build the SSR bundle and put it on a
313streaming Lambda Function URL:
314[Astro](/aws/frontend/astro),
315[SvelteKit](/aws/frontend/sveltekit),
316[Octane](/aws/frontend/octane),
317[Nuxt](/aws/frontend/nuxt),
318[Next.js](/aws/frontend/nextjs),
319[TanStack Start](/aws/frontend/tanstack-start),
320[React Router](/aws/frontend/react-router), and
321[Waku](/aws/frontend/waku).
322
323For any other build command that emits a directory of files, use
324[StaticSite](/aws/frontend/static-site).