Skip to content

Foldkit

Foldkit is an Elm-architecture frontend framework built on Effect. Its apps are client-only Vite projects — the Foldkit Vite plugin only adds HMR and devtools wiring — so AWS.Website.Foldkit deploys one with a single declaration: the client build lands in a private S3 bucket and serves through CloudFront, routed at the edge. There is no server function, no build command, and no CloudFormation to write.

Foldkit is AWS.Website.Vite with Foldkit’s defaults applied: client-side routing is assumed, so deep links fall back to the app shell without you configuring anything.

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

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.

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.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
bun alchemy deploy

A Foldkit app that uses URL routing (Runtime.makeApplication with route, 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",
});

A Foldkit app has no server, so anything it needs is baked into the bundle at build time. Vite inlines VITE_-prefixed variables from the environment the build runs in — a .env file in the project root, or the shell that runs alchemy deploy:

.env
VITE_API_URL=https://api.example.com

Type it for your Foldkit code with Vite’s standard ImportMetaEnv augmentation:

src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}

Client code reads it as import.meta.env.VITE_API_URL, e.g. from a Command that fetches your API.

A value produced by another resource in the Stack (a Function URL, a table name) is not known when this build runs, and the resource takes no env prop. Either serve it from an API the app fetches at runtime, or deploy with AWS.Website.StaticSite instead — it runs the build as a command and passes environment to it, so an Output can be inlined at build time.

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: {
base: "/docs/",
outDir: "build",
},
});

base is the public path the site deploys under (it also applies to the dev server under alchemy dev); 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. config selects an alternate config file, relative to rootDir.

alchemy dev runs the app’s own Vite dev server, so Foldkit’s HMR with state preservation 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 };
});