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 Cloudflare.Website.Foldkit deploys them with a single declaration: no main entrypoint, no build command, no output directory, no Wrangler configuration.

Foldkit is Cloudflare.Website.Vite with Foldkit’s defaults applied: client-side routing is assumed, so deep links fall back to index.html without you configuring anything.

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 and layers its Cloudflare integration on top of this config — 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 Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Foldkit("Foldkit");

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

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

Yield the class from your Stack and return its URL — see examples/cloudflare-foldkit for the checked-in example:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"CloudflareFoldkitExample",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const worker = yield* Website;
return {
url: worker.url,
};
}),
);

Alchemy builds the client assets and serves them from a Worker at the returned url.

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 server as a request for a file that doesn’t exist.

Foldkit defaults assets.notFoundHandling to "single-page-application", which returns index.html for unmatched paths instead of a 404 — the Foldkit runtime resolves the route once the app boots.

A Foldkit app that ships a real 404 page overrides the default with "404-page" — Workers Assets serves the nearest 404.html:

export const Website = Cloudflare.Website.Foldkit("Foldkit", {
assets: {
notFoundHandling: "404-page",
},
});

A Foldkit SPA has no server, so anything it needs from the rest of your Stack is baked into the bundle at build time — pass a VITE_-prefixed key in env:

alchemy.run.ts
export const Website = Cloudflare.Website.Foldkit("Foldkit", {
env: {
VITE_API_URL: backend.url.as<string>(),
},
});

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 it). See Environment for the full inlining semantics.

A Foldkit deployment is pure assets by default — no Worker code runs on a request. Point main at your own module when the deployment must do something at the edge: serve an API route, wrap the app in error reporting, or export Durable Object classes.

The module builds through Vite’s ssr environment and becomes the deployed Worker entry. The client build is still served through the Worker’s ASSETS binding, so the SPA fallback keeps working behind it:

src/worker.ts
type Env = {
ASSETS: { fetch(request: Request): Promise<Response> };
TICKER: KVNamespace;
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/ticker") {
const body = await env.TICKER.get("ticker:clubs");
return new Response(body, {
headers: { "content-type": "application/json" },
});
}
return env.ASSETS.fetch(request);
},
};

Your vite.config.ts needs no changes — the injected Cloudflare plugin points the ssr environment at main itself. Just declare the entry and bind what the Worker needs:

alchemy.run.ts
const ticker = yield* Cloudflare.KV.Namespace("Ticker");
export const Website = Cloudflare.Website.Foldkit("Foldkit", {
main: "src/worker.ts",
env: { TICKER: ticker },
assets: {
runWorkerFirst: ["/api/*"],
},
});

Bindings passed in env are reachable from this Worker entry (and from cron handlers), not from browser code — a Foldkit app runs on the client, so anything it needs must come from a route the Worker serves. Type the Worker’s env with Cloudflare.InferEnv when you want the bindings inferred rather than hand-written.

alchemy dev runs the app’s own Vite dev server, so Foldkit’s HMR with state preservation and its devtools wiring work unchanged. Bindings are live, so a custom main entry sees real KV namespaces and secrets.

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

export const Website = Cloudflare.Website.Foldkit("Foldkit", {
dev: { host: "127.0.0.1", port: 5180, strictPort: true },
});