Skip to content

Foldkit

AWS.Website.Foldkit deploys a Foldkit app to AWS. It supports browser-only SPAs, server-side rendering (SSR), static site generation (SSG), and hybrid rendering. Foldkit’s Vite plugin generates the handler and assets; Alchemy packages them for deployment.

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

Terminal window
pnpm add -D https://pkg.alchemy.run/@alchemy.run/frontend-frameworks/pr:2930:a10573e

Your Vite config stays what Foldkit’s setup gives you:

vite.config.ts
import { foldkit } from "@foldkit/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [foldkit()],
optimizeDeps: {
entries: ["src/entry.ts"],
},
});

Alchemy runs Vite programmatically on the project root, so the Foldkit plugin and the rest of your setup are preserved as-is.

Configure rendering in the application’s foldkit(...) plugin. This example uses Foldkit 0.167.0, @foldkit/vite-plugin 0.27.0 and compatible Effect dependencies:

vite.config.ts
import { foldkit } from "@foldkit/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
foldkit({
ssr: {
serverEntry: "/src/entry.server.ts",
clientEntry: "/src/entry.ts",
build: true,
},
}),
],
});

The server entry exports renderPage and renderDocument. Import stylesheets from the client entry and use Runtime.hydrate to start the browser application. Remove the source index.html for this build: Foldkit generates the complete HTML document, including the client script, stylesheets and module preload links.

src/entry.server.ts
import { Server } from "foldkit/experimental";
export const renderDocument = Server.renderDocument;

Keep your application’s renderPage export alongside the document renderer. Foldkit currently exposes its server rendering API through foldkit/experimental; see its server rendering guide for the application and hydration contract.

For static site generation (SSG), change build to { prerender: true } and export the URLs to generate from the server entry:

src/entry.server.ts
export const prerenderPaths = ["/", "/about"];

SSG renders those URLs during the build. SSR renders a page when its request arrives. A hybrid application prerenders some URLs and renders others on request. The same renderPage and renderDocument exports serve both delivery policies.

AWS chooses its deployment resources before building. Keep the default for a browser-only SPA; select output: "server" for request-time rendering:

alchemy.run.ts
export const Website = AWS.Website.Foldkit("Website", {
output: "server",
});

CloudFront serves assets and prerendered pages from S3. Requests that match no file, and methods other than GET or HEAD, reach the server: a Lambda Web Function by default, or a Lambda Function with server: "function". The application owns its response status and headers through renderPage.

For static-only SSG, deploy the prerendered client output without a server:

alchemy.run.ts
export const Website = AWS.Website.Foldkit("Website", {
output: "static",
errorPage: "404.html",
});

Supply public/404.html for this example. URLs that were not generated return 404; browser interaction still works through hydration. output: "server" retains the handler even when all known pages were prerendered. A server build with no output choice fails rather than discarding its handler. spa and errorPage apply only to assets-only deployments.

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

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

For an app in a subdirectory of a monorepo, point rootDir at it:

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

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(
"MyFoldkitSite",
{
providers: AWS.providers(),
state: AWS.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

One declaration expands into the whole stack — an S3 bucket, the asset upload, a CloudFront KeyValueStore holding the file manifest, a viewer-request CloudFront Function, and the distribution itself.

Terminal window
pnpm alchemy deploy

A browser-only Foldkit app that uses URL routing (Runtime.makeApplication with routing, onUrlRequest, and onUrlChange) resolves routes on the client, so a deep link like /counter/42 arrives at the edge as a request for a file that does not exist.

Foldkit sets spa by default, which answers those misses with index.html and a 200 — the Foldkit runtime resolves the route once the app boots.

An app that ships a real 404 page opts out with errorPage. The two are mutually exclusive:

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

Set server environment variables through env when deploying a server build:

alchemy.run.ts
export const Website = AWS.Website.Foldkit("Website", {
output: "server",
env: { API_BASE: "https://api.example.com" },
});

Server code reads these values from process.env. Alchemy applies them to the native dev server under alchemy dev and to the server on deploy.

An Output from another resource can be passed inside the Stack generator:

Effect.gen(function* () {
const api = yield* Api;
const site = yield* AWS.Website.Foldkit("Website", {
output: "server",
env: { API_BASE: api.url },
});
return { url: site.url };
});

Vite inlines VITE_* values as import.meta.env.VITE_* during the build. Set browser configuration in the app’s .env file or the deployment environment:

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

Values forwarded through env are also available to the build. A VITE_* key becomes public browser configuration; keep credentials in unprefixed server variables. Values serialized into Foldkit Flags also reach the browser.

Vite configuration — the Foldkit plugin included — lives in your project’s own vite.config.*. The vite bag is for deploy-time overrides merged over that file, for values decided by the deployment (config files can’t consume alchemy Outputs):

export const Website = AWS.Website.Foldkit("Docs", {
vite: {
outDir: "build",
},
});

outDir is relative to rootDir. For server builds, Alchemy places client and server outputs under client/ and server/ in this directory. Without an override, the app’s resolved output directories are used. config selects an alternate Vite configuration file relative to rootDir.

alchemy dev runs the app’s own Vite dev server, so Foldkit’s state-preserving live reload and its devtools wiring work unchanged. 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.

Pin the dev server’s address when several apps run in one Stack:

export const Website = AWS.Website.Foldkit("Website", {
dev: { host: "127.0.0.1", port: 5180, strictPort: true },
});
alchemy.run.ts
export const Website = AWS.Website.Foldkit("Website", {
domain: { name: "app.example.com" },
});

The hosted zone is inferred from the hostname. Pass hostedZoneId to pin it when several zones could match.

Serve several sites under one distribution by pointing domain.router at an existing AWS.Website.Router. 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", {});
const site = yield* AWS.Website.Foldkit("Website", {
domain: { router, name: "app.example.com" },
});
return { url: site.url };
});