1---
2title: Foldkit
3description: Deploy a Foldkit app to AWS with AWS.Website.Foldkit — the client build on S3 + CloudFront, SPA deep links by default, and Foldkit's own Vite dev server under alchemy dev.
4sidebar:
5 order: 3
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12[Foldkit](https://foldkit.dev) is an Elm-architecture frontend framework
13built on Effect. Its apps are client-only Vite projects — the Foldkit
14Vite plugin only adds HMR and devtools wiring — so
15`AWS.Website.Foldkit` deploys one with a single declaration: the client
16build lands in a private S3 bucket and serves through CloudFront, routed
17at the edge. There is no server function, no build command, and no
18CloudFormation to write.
19
20`Foldkit` is [`AWS.Website.Vite`](/aws/frontend/vite) with
21Foldkit's defaults applied: client-side routing is assumed, so deep
22links fall back to the app shell without you configuring anything.
23
24## Install
25
26The build integration is not bundled with alchemy. Install
27`@alchemy.run/frontend-frameworks`; the resource loads its `/vite` and
28`/vite/aws` exports from your project at deploy time. It is only used at
29build time, so a dev dependency is enough:
30
31<Tabs syncKey="pkgManager">
32 <TabItem label="bun" icon="bun">
33 <Code code={`bun add -d ${devDeps}`} lang="sh" />
34 </TabItem>
35 <TabItem label="npm" icon="npm">
36 <Code code={`npm install -D ${devDeps}`} lang="sh" />
37 </TabItem>
38 <TabItem label="pnpm" icon="pnpm">
39 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
40 </TabItem>
41 <TabItem label="yarn" icon="seti:yarn">
42 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
43 </TabItem>
44</Tabs>
45
46## Configure Vite
47
48Your Vite config stays what Foldkit's setup gives you:
49
50```typescript
51// vite.config.ts
52import { foldkit } from "@foldkit/vite-plugin";
53import { defineConfig } from "vite";
54
55export default defineConfig({
56 plugins: [foldkit()],
57 optimizeDeps: {
58 entries: ["src/entry.ts"],
59 },
60});
61```
62
63Alchemy runs Vite programmatically on the project root, so the Foldkit
64plugin and the rest of your setup are preserved as-is.
65
66:::note[Pinned Effect versions]
67Foldkit pins its `effect` peer dependency to an exact version. Alchemy
68runs your app's own Vite build with your app's own `node_modules`, so
69that pin is between Foldkit and your package manager — Alchemy imposes
70no Effect version on your frontend.
71:::
72
73## Declare the Website
74
75Declare the site as a module-level const (rather than inline in the
76Stack):
77
78```typescript
79// alchemy.run.ts
80import * as AWS from "alchemy/AWS";
81
82export const Website = AWS.Website.Foldkit("Website");
83```
84
85For an app in a subdirectory of a monorepo, point `rootDir` at it:
86
87```typescript
88export const Website = AWS.Website.Foldkit("Website", {
89 rootDir: "applications/web",
90});
91```
92
93## Add it to the Stack
94
95Yield the site from your Stack and return its URL:
96
97```typescript
98// alchemy.run.ts
99import * as Alchemy from "alchemy";
100import * as Effect from "effect/Effect";
101
102export default Alchemy.Stack(
103 "MyFoldkitSite",
104 {
105 providers: AWS.providers(),
106 state: AWS.state(),
107 },
108 Effect.gen(function* () {
109 const site = yield* Website;
110 return { url: site.url };
111 }),
112);
113```
114
115One declaration expands into the whole stack — an S3 bucket, the asset
116upload, a CloudFront KeyValueStore holding the file manifest, a
117viewer-request CloudFront Function, and the distribution itself.
118
119```sh
120bun alchemy deploy
121```
122
123## Deep links
124
125A Foldkit app that uses URL routing (`Runtime.makeApplication` with
126`route`, `onUrlRequest`, and `onUrlChange`) resolves routes on the
127client, so a deep link like `/counter/42` arrives at the edge as a
128request for a file that does not exist.
129
130`Foldkit` sets `spa` by default, which answers those misses with
131`index.html` and a `200` — the Foldkit runtime resolves the route once
132the app boots.
133
134An app that ships a real 404 page opts out with `errorPage`. The two are
135mutually exclusive:
136
137```typescript
138export const Website = AWS.Website.Foldkit("Website", {
139 spa: false,
140 errorPage: "404.html",
141});
142```
143
144## Add environment variables
145
146A Foldkit app has no server, so anything it needs is baked into the
147bundle at build time. Vite inlines `VITE_`-prefixed variables from the
148environment the build runs in — a `.env` file in the project root, or
149the shell that runs `alchemy deploy`:
150
151```sh
152# .env
153VITE_API_URL=https://api.example.com
154```
155
156Type it for your Foldkit code with Vite's standard `ImportMetaEnv`
157augmentation:
158
159```typescript
160// src/vite-env.d.ts
161/// <reference types="vite/client" />
162
163interface ImportMetaEnv {
164 readonly VITE_API_URL: string;
165}
166```
167
168Client code reads it as `import.meta.env.VITE_API_URL`, e.g. from a
169`Command` that fetches your API.
170
171A value produced by another resource in the Stack (a Function URL, a
172table name) is not known when this build runs, and the resource takes no
173`env` prop. Either serve it from an
174[API](/aws/apis/effect-http-api) the app fetches at runtime, or deploy
175with [`AWS.Website.StaticSite`](/aws/frontend/static-site) instead —
176it runs the build as a command and passes `environment` to it, so an
177Output can be inlined at build time.
178
179## Deploy-time overrides
180
181Vite configuration — the Foldkit plugin included — lives in your
182project's own `vite.config.*`. The `vite` bag is for deploy-time
183overrides merged *over* that file, for values decided by the
184deployment (config files can't consume alchemy `Output`s):
185
186```typescript
187export const Website = AWS.Website.Foldkit("Docs", {
188 vite: {
189 base: "/docs/",
190 outDir: "build",
191 },
192});
193```
194
195`base` is the public path the site deploys under (it also applies to
196the dev server under `alchemy dev`); `outDir` is the build output
197directory, relative to `rootDir`. Leave the bag unset and the resolved
198values from your `vite.config.*` are used — Vite's own `outDir`
199default is `dist`. `config` selects an alternate config file, relative
200to `rootDir`.
201
202## Local dev
203
204`alchemy dev` runs the app's own Vite dev server, so Foldkit's HMR with
205state preservation and its devtools wiring work unchanged. `site.url` is
206the local address and no AWS resources are created. Wrap the site in
207`Alchemy.remote()` to deploy the real infrastructure even during dev.
208
209Pin the dev server's address when several apps run in one Stack:
210
211```typescript
212export const Website = AWS.Website.Foldkit("Website", {
213 dev: { host: "127.0.0.1", port: 5180, strictPort: true },
214});
215```
216
217## Custom domain
218
219```typescript
220// alchemy.run.ts
221export const Website = AWS.Website.Foldkit("Website", {
222 domain: { name: "app.example.com" },
223});
224```
225
226The hosted zone is inferred from the hostname. Pass `hostedZoneId` to
227pin it when several zones could match.
228
229Serve several sites under one distribution by pointing `domain.router`
230at an existing
231[`AWS.Website.Router`](/aws/frontend/static-site#compose-sites-with-a-router).
232The site takes the Router's outputs, so both are yielded inside the
233Stack generator:
234
235```typescript
236Effect.gen(function* () {
237 const router = yield* AWS.Website.Router("Router", {});
238
239 const site = yield* AWS.Website.Foldkit("Website", {
240 domain: { router, name: "app.example.com" },
241 });
242
243 return { url: site.url };
244});
245```
246
247:::note[No edge code]
248Cloudflare's `Foldkit` accepts a `main` entry that runs a Worker in
249front of the assets. AWS has no equivalent: this composite is
250assets-only and never creates a server function. Put request-time logic
251behind its own [Lambda API](/aws/apis/effect-http-api) and fetch it from
252the client.
253:::