Skip to content

AWS.Website reference

Source: src/AWS/Website/AssetDeployment.ts

Upload a local directory into S3 with website-friendly defaults.

AssetDeployment is a helper resource for website hosting. It uploads all files in a directory, infers content types, applies cache-control defaults (HTML is never cached; everything else is immutable), and can optionally purge stale files under a prefix. StaticSite and SsrSite use it internally — reach for it directly when you manage the bucket and CDN yourself.

Upload A Build Directory

const bucket = yield* AWS.S3.Bucket("SiteBucket", {});
const files = yield* AssetDeployment("WebsiteFiles", {
bucket,
sourcePath: "./dist",
prefix: "_assets",
});

Purge Stale Files And Override Caching

const files = yield* AssetDeployment("WebsiteFiles", {
bucket,
sourcePath: "./dist",
purge: true,
fileOptions: [
{
files: "**\/*.json",
cacheControl: "max-age=300,public",
},
],
});
const files = yield* AssetDeployment("WebsiteFiles", {
bucket,
sourcePath: "./dist",
});
// `version` only changes when file contents change, so the
// invalidation re-runs exactly when a new deploy ships new bytes.
yield* AWS.CloudFront.Invalidation("Invalidation", {
distributionId: distribution.distributionId,
version: files.version,
paths: ["/*"],
});

Source: src/AWS/Website/Astro.ts

Deploy an Astro application to AWS: the server bundle on a streaming Lambda Function URL, static assets (prerendered pages included) in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/astro with the @alchemy.run/frontend-frameworks/astro/aws deploy target (a wrangler-free AWS Lambda adapter is injected — your astro.config.* must not declare one) — the package must be installed in your project.

Pages render on demand by default (output: "server"); pages that export const prerender = true are prerendered at build time and served from S3. With astro: { output: "static" } every page is prerendered and the deploy is assets-only — no Lambda.

Your astro.config.* is the home for Astro configuration and loads natively; the astro prop is a deploy-time override bag merged OVER the file for values that vary per stage or derive from other resources’ Outputs. config points at an alternate config file (relative to rootDir).

Basic Astro App

const site = yield* AWS.Website.Astro("Web", {
rootDir: "./app",
});

Per-stage site URL override

const site = yield* AWS.Website.Astro("Blog", {
rootDir: "./blog",
astro: { site: "https://blog.example.com" },
});

Custom Domain

const site = yield* AWS.Website.Astro("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Astro("Docs", {
rootDir: "./docs",
astro: { output: "static" },
errorPage: "404.html",
});
const site = yield* AWS.Website.Astro("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Source: src/AWS/Website/Foldkit.ts

Deploy a Foldkit app to AWS: the client build in S3 behind a CloudFront distribution. Foldkit is an Elm-architecture frontend framework built on Effect, and its apps are client-only Vite projects — so the deployment is assets-only and never creates a server function.

The Foldkit Vite plugin lives in your project’s own vite.config.*, which loads natively. The build runs through @alchemy.run/frontend-frameworks/vite with the @alchemy.run/frontend-frameworks/vite/aws deploy target — the package must be installed in your project. Input files are content-hashed so unchanged projects skip the build and deploy entirely.

Foldkit apps route on the client, so spa defaults on: unmatched paths serve index.html with a 200 and the Foldkit runtime resolves the route once the app boots.

During alchemy dev the site is Vite’s own dev server — Foldkit’s HMR and devtools wiring work unchanged — and no AWS resources are created. Alchemy.remote() opts back into the full deployment.

Basic Foldkit App

const site = yield* AWS.Website.Foldkit("Web");

Project in a Subdirectory

const site = yield* AWS.Website.Foldkit("Web", {
rootDir: "applications/web",
});

Custom Domain

const site = yield* AWS.Website.Foldkit("Web", {
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});

A deep link like /counter/42 arrives at the edge as a request for a file that does not exist. spa is on by default so the shell is served instead of a 404. An app that ships a real 404 page opts out with errorPage — the two are mutually exclusive.

const site = yield* AWS.Website.Foldkit("Web", {
spa: false,
errorPage: "404.html",
});
const router = yield* AWS.Website.Router("Router", {});
const site = yield* AWS.Website.Foldkit("Web", {
domain: { router },
});

Vite configuration (the Foldkit plugin included) lives in your project’s own vite.config.*. The vite bag holds deploy-time overrides merged over that file, and config selects an alternate config file.

const site = yield* AWS.Website.Foldkit("Web", {
vite: { base: "/app/" },
});
// `alchemy dev` starts `vite` programmatically: site.url is the local
// dev server (Foldkit HMR included); no bucket or distribution is
// created.
const site = yield* AWS.Website.Foldkit("Web");

Source: src/AWS/Website/Nextjs.ts

Deploy a Next.js application to AWS with the OpenNext (@opennextjs/aws) serverless topology: the SSR server on a streaming Lambda Function URL, static assets in S3 behind CloudFront’s KV-manifest edge router, the ISR/fetch cache in a dedicated S3 bucket, a dedicated image optimization Lambda routed at /_next/image, and ISR revalidation through an SQS FIFO queue plus a DynamoDB tag-cache table.

The build runs through @alchemy.run/frontend-frameworks/nextjs/aws (the @opennextjs/aws pipeline) — both it and @opennextjs/aws must be installed in your project. When the project has no open-next.config.ts, a minimal default with the streaming server wrapper is generated.

During alchemy dev the site is Next’s own dev server (next dev) and no cloud resources are declared; Alchemy.remote() opts back into the full live deployment.

Basic Next.js App

const site = yield* AWS.Website.Nextjs("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.Nextjs("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Nextjs("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Source: src/AWS/Website/Nuxt.ts

Deploy a Nuxt application to AWS: the nitro server on a streaming Lambda Function URL, static assets (prerendered pages included) in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/nuxt with the @alchemy.run/frontend-frameworks/nuxt/aws deploy target (nitro’s aws-lambda preset, streaming enabled) — both must be installed in your project.

Basic Nuxt App

const site = yield* AWS.Website.Nuxt("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.Nuxt("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Nuxt("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
NUXT_PUBLIC_API_BASE: api.url,
},
});

nuxt.config.ts is the primary home for Nuxt configuration — it loads natively. The nuxt prop layers deploy-time overrides on top (the highest-priority c12 layer) for values the file can’t express, like per-stage settings. The bag must be JSON-serializable — no functions, plugins, or modules — and nitro.preset stays owned by the deploy target.

const site = yield* AWS.Website.Nuxt("Web", {
rootDir: "./app",
nuxt: {
app: { baseURL: "/docs/" },
runtimeConfig: {
public: { apiBase: "https://api.example.com" },
},
},
});

Source: src/AWS/Website/Octane.ts

Deploy an OctaneJS application to AWS: Octane’s SSR server on a streaming Lambda Function URL, static assets in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/octane with the @alchemy.run/frontend-frameworks/octane/aws deploy target — the project’s own vite build (with @octanejs/vite-plugin) produces the default native Node server bundle, and the target’s finishing pass automatically wraps its fetch handler as a streaming Lambda handler. AWS.Website.Octane selects hosting; keep native compiler and route settings in octane.config.ts without an adapter. The legacy AWS marker adapter remains optional for existing projects.

Basic Octane App

const site = yield* AWS.Website.Octane("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.Octane("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Octane("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Source: src/AWS/Website/ReactRouter.ts

Deploy a React Router v7 app (framework mode) to AWS: the SSR server on a streaming Lambda Function URL, client assets in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/react-router with the @alchemy.run/frontend-frameworks/react-router/aws deploy target — both must be installed in your project, alongside @react-router/dev, react-router, and vite.

Your vite.config.ts needs no adapter wiring. React Router’s server build is a ServerBuild manifest rather than a request handler, so the integration makes the server pass’s build input a module that wraps the manifest with createRequestHandler, forces the SSR bundle to be self-contained, and packages the resulting fetch handler as a streaming Lambda handler.

React Server Components (React Router’s unstable RSC plugin) and multi-environment builds are not supported yet — the build fails with an actionable error when more than one server entry is emitted.

Basic React Router App

const site = yield* AWS.Website.ReactRouter("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.ReactRouter("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});

Tune The Server Function

const site = yield* AWS.Website.ReactRouter("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Read An Environment Variable From A Loader

app/routes/home.tsx
export function loader() {
return { apiBase: process.env.API_BASE ?? "unset" };
}
const router = yield* AWS.Website.Router("FrontDoor", {});
const site = yield* AWS.Website.ReactRouter("Web", {
rootDir: "./app",
domain: { router },
});

Source: src/AWS/Website/Router.ts

Shared CloudFront front door with KV-based dynamic routing.

Router owns a single CloudFront distribution with a placeholder origin. Routes are registered lazily via KV entries. A CloudFront Function reads the KV store at the edge and dynamically sets the origin using cf.updateRequestOrigin().

Sites register themselves by writing their file manifest and metadata into the Router’s KV store. The Router’s CF function matches incoming requests to routes by host pattern and path prefix, then delegates to routeSite() for static site routing or directly sets URL/S3 origins.

const router = yield* Router("WebsiteRouter", {
domain: { name: "example.com", hostedZoneId },
});
const router = yield* Router("WebsiteRouter", {
routes: {
"/api/*": { url: api.functionUrl },
"/*": { bucket: assetsBucket },
},
});
const router = yield* Router("WebsiteRouter", {
invalidation: { paths: "all", wait: true },
});
// The site registers itself in the Router's KV store; no new
// distribution is created.
const docs = yield* AWS.Website.StaticSite("DocsSite", {
path: "./docs/dist",
domain: {
router,
path: "/docs",
},
});

Source: src/AWS/Website/SolidStart.ts

Deploy a SolidStart application to AWS: the SSR server on a streaming Lambda Function URL, static assets (prerendered pages included) in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/solidstart with the @alchemy.run/frontend-frameworks/solidstart/aws deploy target — both must be installed in your project, alongside @solidjs/start and @solidjs/vite-plugin-nitro-2.

Your vite.config.ts needs no adapter wiring: the integration drives the project’s own vite build and appends its own nitro plugin instance carrying nitro’s aws-lambda preset with response streaming enabled.

Basic SolidStart App

const site = yield* AWS.Website.SolidStart("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.SolidStart("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.SolidStart("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

The integration owns the nitro plugin instance (a nitroV2Plugin() in your vite.config.* is rejected), so nitro options — prerendering included — go on the nitro prop. Prerendered pages are uploaded to S3 and served from the edge automatically.

const site = yield* AWS.Website.SolidStart("Web", {
rootDir: "./app",
nitro: { prerender: { routes: ["/", "/about"] } },
});

Source: src/AWS/Website/SsrSite.ts

A server-rendered website behind CloudFront.

SsrSite serves a dynamic origin behind CloudFront and can optionally split immutable static assets into a private S3 bucket origin.

Lambda URL Origin

const site = yield* SsrSite("App", {
server: {
type: "lambda",
function: appFunction,
},
});

SSR With Static Assets

const site = yield* SsrSite("App", {
server: {
type: "lambda",
function: appFunction,
},
assets: {
sourcePath: "./dist/client",
},
});
const site = yield* SsrSite("App", {
server: {
type: "ecs",
service: webService,
},
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
// Skip the standalone distribution and register the returned
// routeTargets on an AWS.Website.Router instead.
const site = yield* SsrSite("App", {
server: {
type: "lambda",
function: appFunction,
},
cdn: false,
});
const router = yield* AWS.Website.Router("WebsiteRouter", {
routes: {
"/*": site.routeTargets.server,
},
});

Source: src/AWS/Website/StaticSite.ts

Deploy a static website to S3 and CloudFront using KV-based edge routing.

StaticSite uploads site files to a private S3 bucket, creates a CloudFront KeyValueStore with a file manifest for edge routing, and optionally builds the site first. Supports standalone distribution or composition with AWS.Website.Router.

const site = yield* StaticSite("Docs", {
path: "./site",
});
const site = yield* StaticSite("Web", {
path: "./frontend",
build: {
command: "bun run build",
output: "dist",
env: {
VITE_API_URL: api.url,
},
},
});
// Misses fall back to index.html with a 200 so the client router
// can handle the path.
const site = yield* StaticSite("App", {
path: "./app",
build: {
command: "bun run build",
output: "dist",
},
spa: true,
});
const site = yield* StaticSite("Web", {
path: "./site",
domain: {
name: "www.example.com",
hostedZoneId: zone.hostedZoneId,
},
errorPage: "404.html",
});

Serve Through A Router

const site = yield* StaticSite("Docs", {
path: "./docs",
domain: {
router,
path: "/docs",
},
});

Host-Matched Router Attachment

// The site serves for docs.example.com on the router. On a same-stack
// router that owns a domain, this declaration alone provisions the
// hostname end-to-end: the site binds it onto the router's distribution
// (alias), certificate (SAN), and Route 53 record set. Wildcard
// patterns and cross-stack router refs register KV host-matching only —
// those hostnames must be covered by the router's own domain.
const site = yield* StaticSite("Docs", {
path: "./docs",
domain: {
name: "docs.example.com",
router,
},
});

Source: src/AWS/Website/SvelteKit.ts

Deploy a SvelteKit application to AWS: kit’s SSR server on a streaming Lambda Function URL, static assets (prerendered pages included) in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/sveltekit with the @alchemy.run/frontend-frameworks/sveltekit/aws deploy target (an in-memory kit adapter emitting a streaming Lambda handler) — both must be installed in your project.

Basic SvelteKit App

const site = yield* AWS.Website.SvelteKit("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.SvelteKit("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.SvelteKit("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Kit options live in the sveltekit(...) call in your vite.config.ts, which loads natively. The kit prop is a deploy-time override bag merged over your own options (the prop wins) — useful for per-stage values the config file can’t compute. JSON-serializable values only.

const site = yield* AWS.Website.SvelteKit("Web", {
rootDir: "./app",
kit: {
paths: { base: "/docs" },
},
});

Source: src/AWS/Website/TanStackStart.ts

Deploy a TanStack Start application to AWS: the SSR server on a streaming Lambda Function URL, client assets in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/tanstack-start with the @alchemy.run/frontend-frameworks/tanstack-start/aws deploy target — both must be installed in your project, alongside @tanstack/react-start (or @tanstack/solid-start) and vite.

Your vite.config.ts needs no adapter wiring: TanStack Start is pure Vite, so the integration drives the project’s own vite build, forces the SSR bundle to be self-contained, and wraps its fetch handler as a streaming Lambda handler.

TanStackStart: Creating TanStack Start Sites

Section titled “TanStackStart: Creating TanStack Start Sites”

Basic TanStack Start App

const site = yield* AWS.Website.TanStackStart("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.TanStackStart("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});

Tune The Server Function

const site = yield* AWS.Website.TanStackStart("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Read An Environment Variable From A Server Function

src/routes/index.tsx
const getApiBase = createServerFn({ method: "GET" }).handler(() => ({
apiBase: process.env.API_BASE,
}));
const router = yield* AWS.Website.Router("FrontDoor", {});
const site = yield* AWS.Website.TanStackStart("Web", {
rootDir: "./app",
domain: { router },
});

Source: src/AWS/Website/Vinext.ts

Deploy a vinext App Router application to AWS: the RSC server on a streaming Lambda Function URL, static assets in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

ISR / "use cache" persistence is an S3 bucket (not Cloudflare KV). This resource provisions the bucket, sets CACHE_BUCKET_NAME, and grants the Lambda Get/Put/Delete/List. Alchemy injects the S3 data-cache adapter during the build; no Alchemy plugin is needed in vite.config.ts. A dedicated cache bucket keeps the graph acyclic (the asset bucket is bound to CloudFront).

Requires vinext 1.0.0-beta.10 or newer. The Lambda build uses vinext’s standalone packaging to include external runtime dependencies. Pages Router applications require a Node deployment target instead of Lambda.

This is not AWS.Website.Nextjs (OpenNext) and not Cloudflare.Website.Vinext. During alchemy dev the site is vinext dev and no cloud resources are declared.

Basic vinext App

const site = yield* AWS.Website.Vinext("Web", {
rootDir: "./app",
});

vite.config.ts (platform-agnostic)

import { defineConfig } from "vite";
import vinext from "vinext";
export default defineConfig({
plugins: [vinext({ prerender: true })],
});

Source: src/AWS/Website/Vite.ts

Deploy a plain Vite application to AWS: static assets (the vite build output) in S3 behind a CloudFront distribution. For client-only projects — React/Vue/Solid SPAs, index.html multi-page apps — whose entire deployable output is static assets.

The build runs through @alchemy.run/frontend-frameworks/vite with the @alchemy.run/frontend-frameworks/vite/aws deploy target — the package must be installed in your project. Your project’s own vite.config.* (plugins included) drives the build; input files are content-hashed so unchanged projects skip the build and deploy entirely.

During alchemy dev the site is Vite’s own dev server (native HMR) and no cloud resources are created — the site’s url is the dev server’s local address. Alchemy.remote() opts back into the full deployment.

SSR frameworks that wrap Vite deploy through their own composites (AWS.Website.Astro, AWS.Website.SvelteKit, AWS.Website.Octane, …) — this composite never creates a server function.

Basic Vite SPA

const site = yield* AWS.Website.Vite("Web");

Project in a Subdirectory

const site = yield* AWS.Website.Vite("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.Vite("Web", {
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Vite("Docs", {
spa: false,
errorPage: "404.html",
});
const router = yield* AWS.Website.Router("Router", {});
const site = yield* AWS.Website.Vite("Web", {
domain: { router },
});

Vite configuration lives in your project’s own vite.config.*, which loads natively — plugins included. The vite bag is for deploy-time overrides merged over that file (config files cannot consume alchemy Outputs), and config selects an alternate config file.

Deploy-Time Base Path Override

const site = yield* AWS.Website.Vite("Docs", {
vite: { base: "/docs/" },
});

Alternate Config File

const site = yield* AWS.Website.Vite("Web", {
config: "vite.deploy.config.ts",
});
// `alchemy dev` starts `vite` programmatically: site.url is the local
// dev server (HMR included); no bucket or distribution is created.
const site = yield* AWS.Website.Vite("Web");

Source: src/AWS/Website/Waku.ts

Deploy a Waku application to AWS: the RSC server on a streaming Lambda Function URL, static assets (SSG pages included) in S3, and a CloudFront distribution whose edge router serves uploaded files from S3 and forwards everything else to the server.

The build runs through @alchemy.run/frontend-frameworks/waku with the @alchemy.run/frontend-frameworks/waku/aws deploy target (this package’s fork of waku’s aws-lambda adapter, streaming enabled) — both must be installed in your project.

Basic Waku App

const site = yield* AWS.Website.Waku("Web", {
rootDir: "./app",
});

Custom Domain

const site = yield* AWS.Website.Waku("Web", {
rootDir: "./app",
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});
const site = yield* AWS.Website.Waku("Web", {
rootDir: "./app",
memorySize: 2048,
env: {
API_BASE: api.url,
},
});

Waku configuration lives in your waku.config.*. The waku prop overrides it per key at deploy time — for values that vary by stage or come from other resources.

const site = yield* AWS.Website.Waku("Web", {
waku: {
basePath: "/docs/",
},
});