1---
2title: Foldkit
3description: Deploy a Foldkit app to Cloudflare with the Foldkit resource — one declaration, no Wrangler config.
4sidebar:
5 order: 8
6---
7
8[Foldkit](https://foldkit.dev) is an Elm-architecture frontend
9framework built on Effect. Its apps are client-only Vite projects —
10the Foldkit Vite plugin only adds HMR and devtools wiring — so
11`Cloudflare.Website.Foldkit` deploys them with a single declaration:
12no `main` entrypoint, no build command, no output directory, no
13Wrangler configuration.
14
15`Foldkit` is [`Cloudflare.Website.Vite`](/cloudflare/frontend/vite)
16with Foldkit's defaults applied: client-side routing is assumed, so
17deep links fall back to `index.html` without you configuring
18anything.
19
20## Configure Vite
21
22Your Vite config stays what Foldkit's setup gives you:
23
24```typescript
25// vite.config.ts
26import { foldkit } from "@foldkit/vite-plugin";
27import { defineConfig } from "vite";
28
29export default defineConfig({
30 plugins: [foldkit()],
31 optimizeDeps: {
32 entries: ["src/entry.ts"],
33 },
34});
35```
36
37Alchemy runs Vite programmatically on the project root and layers
38its Cloudflare integration on top of this config — the Foldkit
39plugin and the rest of your setup are preserved as-is.
40
41:::note[Pinned Effect versions]
42Foldkit pins its `effect` peer dependency to an exact version.
43Alchemy runs your app's own Vite build with your app's own
44`node_modules`, so that pin is between Foldkit and your package
45manager — Alchemy imposes no Effect version on your frontend.
46:::
47
48## Declare the Website
49
50Declare the site as a module-level const (rather than inline in the
51Stack):
52
53```typescript
54// alchemy.run.ts
55import * as Cloudflare from "alchemy/Cloudflare";
56
57export const Website = Cloudflare.Website.Foldkit("Foldkit");
58```
59
60For an app in a subdirectory of a monorepo, point `rootDir` at it:
61
62```typescript
63export const Website = Cloudflare.Website.Foldkit("Foldkit", {
64 rootDir: "applications/web",
65});
66```
67
68## Add it to the Stack
69
70Yield the class from your Stack and return its URL — see
71[examples/cloudflare-foldkit](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-foldkit)
72for the checked-in example:
73
74```typescript
75// alchemy.run.ts
76import * as Alchemy from "alchemy";
77import * as Effect from "effect/Effect";
78
79export default Alchemy.Stack(
80 "CloudflareFoldkitExample",
81 {
82 providers: Cloudflare.providers(),
83 state: Cloudflare.state(),
84 },
85 Effect.gen(function* () {
86 const worker = yield* Website;
87
88 return {
89 url: worker.url,
90 };
91 }),
92);
93```
94
95Alchemy builds the client assets and serves them from a Worker at
96the returned `url`.
97
98## Deep links
99
100A Foldkit app that uses URL routing (`Runtime.makeApplication` with
101`route`, `onUrlRequest`, and `onUrlChange`) resolves routes on the
102client, so a deep link like `/counter/42` arrives at the server as a
103request for a file that doesn't exist.
104
105`Foldkit` defaults `assets.notFoundHandling` to
106`"single-page-application"`, which returns `index.html` for unmatched
107paths instead of a 404 — the Foldkit runtime resolves the route once
108the app boots.
109
110A Foldkit app that ships a real 404 page overrides the default with
111`"404-page"` — Workers Assets serves the nearest `404.html`:
112
113```typescript
114export const Website = Cloudflare.Website.Foldkit("Foldkit", {
115 assets: {
116 notFoundHandling: "404-page",
117 },
118});
119```
120
121## Add environment variables
122
123A Foldkit SPA has no server, so anything it needs from the rest of
124your Stack is baked into the bundle at build time — pass a
125`VITE_`-prefixed key in `env`:
126
127```diff lang="typescript"
128// alchemy.run.ts
129export const Website = Cloudflare.Website.Foldkit("Foldkit", {
130+ env: {
131+ VITE_API_URL: backend.url.as<string>(),
132+ },
133});
134```
135
136Type it for your Foldkit code with Vite's standard `ImportMetaEnv`
137augmentation:
138
139```typescript
140// src/vite-env.d.ts
141/// <reference types="vite/client" />
142
143interface ImportMetaEnv {
144 readonly VITE_API_URL: string;
145}
146```
147
148Client code reads it as `import.meta.env.VITE_API_URL` (e.g. from a
149`Command` that fetches it). See
150[Environment](/cloudflare/frontend/vite#environment) for the full
151inlining semantics.
152
153## Add a Worker in front of the app
154
155A Foldkit deployment is pure assets by default — no Worker code runs
156on a request. Point `main` at your own module when the deployment
157must do something at the edge: serve an API route, wrap the app in
158error reporting, or export Durable Object classes.
159
160The module builds through Vite's `ssr` environment and becomes the
161deployed Worker entry. The client build is still served through the
162Worker's `ASSETS` binding, so the SPA fallback keeps working behind
163it:
164
165```typescript
166// src/worker.ts
167type Env = {
168 ASSETS: { fetch(request: Request): Promise<Response> };
169 TICKER: KVNamespace;
170};
171
172export default {
173 async fetch(request: Request, env: Env): Promise<Response> {
174 const url = new URL(request.url);
175
176 if (url.pathname === "/api/ticker") {
177 const body = await env.TICKER.get("ticker:clubs");
178 return new Response(body, {
179 headers: { "content-type": "application/json" },
180 });
181 }
182
183 return env.ASSETS.fetch(request);
184 },
185};
186```
187
188Your `vite.config.ts` needs no changes — the injected Cloudflare
189plugin points the `ssr` environment at `main` itself. Just declare
190the entry and bind what the Worker needs:
191
192```typescript
193// alchemy.run.ts
194const ticker = yield* Cloudflare.KV.Namespace("Ticker");
195
196export const Website = Cloudflare.Website.Foldkit("Foldkit", {
197 main: "src/worker.ts",
198 env: { TICKER: ticker },
199 assets: {
200 runWorkerFirst: ["/api/*"],
201 },
202});
203```
204
205Bindings passed in `env` are reachable from this Worker entry (and
206from cron handlers), not from browser code — a Foldkit app runs on
207the client, so anything it needs must come from a route the Worker
208serves. Type the Worker's `env` with
209[`Cloudflare.InferEnv`](/cloudflare/compute/workers) when you want
210the bindings inferred rather than hand-written.
211
212## Dev
213
214`alchemy dev` runs the app's own Vite dev server, so Foldkit's HMR
215with state preservation and its devtools wiring work unchanged.
216Bindings are live, so a custom `main` entry sees real KV namespaces
217and secrets.
218
219Pin the dev server's address when several apps run in one Stack:
220
221```typescript
222export const Website = Cloudflare.Website.Foldkit("Foldkit", {
223 dev: { host: "127.0.0.1", port: 5180, strictPort: true },
224});
225```