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