Skip to content

Vite

AWS.Website.Vite deploys a client-only Vite project to AWS. It runs your project’s own vite build — your vite.config.* loads natively, plugins included — and serves the output from a private S3 bucket through CloudFront. The site is assets-only: no Lambda is created, no CloudFormation to write, no build command to wire up.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads its /vite and /vite/aws exports from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

There is nothing to configure. Your vite.config.* loads natively — plugins, aliases, and any other non-serializable options work exactly as they do outside Alchemy:

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});

A project with no config file at all works too: an index.html next to your alchemy.run.ts is enough.

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as AWS from "alchemy/AWS";
export const Website = AWS.Website.Vite("Website");

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyViteSite",
{
providers: AWS.providers(),
state: AWS.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

Deploying uploads the build output to S3, writes the file manifest to a CloudFront KeyValueStore, and creates the distribution that serves it at the edge.

Terminal window
bun alchemy deploy

See examples/aws-website-vite for the checked-in example.

rootDir is the Vite project root — the directory holding package.json, vite.config.*, and index.html. It defaults to the directory you run alchemy from:

export const Website = AWS.Website.Vite("Website", {
rootDir: "./web",
});

Vite resolves index.html relative to its root, so index.html must live directly under rootDir. See Vite’s project structure docs.

Your vite.config.* is the home for Vite configuration. The vite bag is for the exception: a value decided by the deployment rather than the project — config files can’t consume alchemy Outputs. Overrides merge over the config file, so the deploy-time value wins:

export const Website = AWS.Website.Vite("Docs", {
vite: {
base: "/docs/",
outDir: "build",
},
});

base is the public path the site deploys under; outDir is the build output directory, relative to rootDir. Leave the bag unset and the resolved values from your vite.config.* are used — Vite’s own outDir default is dist. base also applies to the dev server under alchemy dev.

To load a different config file entirely, pass config (relative to rootDir):

export const Website = AWS.Website.Vite("Web", {
config: "vite.deploy.config.ts",
});

A miss — a request that matches no uploaded file — can be answered two ways, and they are mutually exclusive. Plain Vite apps are usually single-page apps, so spa defaults to true: misses serve the index page with a 200 and the client-side router takes over.

For a multi-page project (an index.html per route), turn it off and point at the built error page instead:

export const Website = AWS.Website.Vite("Docs", {
spa: false,
errorPage: "404.html",
});

errorPage serves that file with a real 404 status. Pretty URLs work in both modes: /about serves about.html or about/index.html when one was uploaded, resolved at the edge from the file manifest.

There is no server, so everything the app reads is inlined into the bundle at build time — plain Vite semantics. Put VITE_-prefixed keys in a .env file at rootDir and read them from import.meta.env:

.env
VITE_API_URL=https://api.example.com
src/api.ts
const apiUrl = import.meta.env.VITE_API_URL;

Only VITE_-prefixed keys reach the bundle. A committed .env is hashed with the rest of the project, so editing one re-runs the build on the next deploy.

The resource takes no env prop, so a value that only exists after another resource deploys — an API’s Function URL, a bucket name — cannot reach this build. Deploy those apps with AWS.Website.StaticSite instead, which runs the build as a command and passes environment to it.

Every non-gitignored file under rootDir plus the nearest lockfile is content-hashed. If nothing changed, both the build and the deploy are skipped. Narrow the scope with memo when the project holds large directories that don’t affect the build output:

export const Website = AWS.Website.Vite("Docs", {
memo: {
include: ["src/**", "index.html", "package.json"],
lockfile: true,
},
});

Setting include flips the lockfile default to false, so pair it with lockfile: true to keep rebuilding when dependencies change. memo: false rebuilds on every deploy. See Memoization for the details.

Every deploy that changes your files invalidates /* and moves on without waiting. Tune it with invalidation:

export const Website = AWS.Website.Vite("Website", {
invalidation: {
paths: "versioned",
wait: true,
},
});

paths: "versioned" invalidates only the index page, for builds whose assets are content-hashed and immutable; invalidation: false skips it entirely.

Terminal window
bun alchemy dev

alchemy dev runs Vite’s own dev server (native HMR) instead of deploying; site.url is the local address and no AWS resources are created. Wrap the site in Alchemy.remote() to deploy the real infrastructure even during dev.

The dev server picks an ephemeral port by default. Pin it to keep the local URL stable across runs:

export const Website = AWS.Website.Vite("Website", {
dev: { port: 3000 },
});
alchemy.run.ts
export const Website = AWS.Website.Vite("Website", {
domain: { name: "app.example.com" },
});

The ACM certificate and Route 53 records are managed for you — the hosted zone is inferred from the hostname, and hostedZoneId pins it when several zones could match. A bare string is shorthand for { name }.

Attach the site to an existing AWS.Website.Router to serve it through one shared distribution instead of its own:

The site takes the Router’s outputs, so both are yielded inside the Stack generator:

Effect.gen(function* () {
const router = yield* AWS.Website.Router("Router", {
domain: { name: "example.com" },
});
const site = yield* AWS.Website.Vite("Docs", {
domain: { router, path: "/docs" },
});
return { url: site.url };
});

Anything whose whole deployable output is a vite build deploys with this resource. Each client-only stack has its own page building on it:

  • React SPA — from a bare index.html to the create-vite template.
  • Vue — Vue 3 SPAs with vue-router deep links.
  • Foldkit — builds through the same Vite pipeline under its own AWS.Website.Foldkit resource.

AWS.Website.Vite is static-only — it never creates a server function. Frameworks that wrap Vite with a server half deploy through their own resources, which build the SSR bundle and put it on a streaming Lambda Function URL: Astro, SvelteKit, Octane, Nuxt, Next.js, TanStack Start, React Router, and Waku.

For any other build command that emits a directory of files, use StaticSite.