Skip to content

Foldkit

Cloudflare.Website.Foldkit deploys a Foldkit app as a Cloudflare Worker. Alchemy runs the project’s Vite build and uploads the client output as Worker static assets. With ssr.build enabled, the generated Fetch handler becomes the Worker entry.

Configure rendering in the application’s Foldkit plugin. The resource supports browser-only SPAs, server-side rendering (SSR), static site generation (SSG), and hybrid rendering. Asset routing follows the build’s output.

Keep the foldkit() plugin from the Foldkit scaffold and your normal Vite plugins:

vite.config.ts
import { foldkit } from "@foldkit/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [foldkit()],
});

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. This is a client-only app: a plain vite build, deployed as assets.

Configure rendering in the application’s foldkit(...) plugin. This example uses Foldkit 0.167.0, @foldkit/vite-plugin 0.27.0 and compatible Effect dependencies:

vite.config.ts
import { foldkit } from "@foldkit/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
foldkit({
ssr: {
serverEntry: "/src/entry.server.ts",
clientEntry: "/src/entry.ts",
build: true,
},
}),
],
});

The server entry exports renderPage and renderDocument. Import stylesheets from the client entry and use Runtime.hydrate to start the browser application. Remove the source index.html for this build: Foldkit generates the complete HTML document, including the client script, stylesheets and module preload links.

src/entry.server.ts
import { Server } from "foldkit/experimental";
export const renderDocument = Server.renderDocument;

Keep your application’s renderPage export alongside the document renderer. Foldkit currently exposes its server rendering API through foldkit/experimental; see its server rendering guide for the application and hydration contract.

For static site generation (SSG), change build to { prerender: true } and export the URLs to generate from the server entry:

src/entry.server.ts
export const prerenderPaths = ["/", "/about"];

SSG renders those URLs during the build. SSR renders a page when its request arrives. A hybrid application prerenders some URLs and renders others on request. The same renderPage and renderDocument exports serve both delivery policies.

The same Website declaration deploys the generated handler and client assets. Prerendered pages are served as files. Requests that match no file reach the handler, where renderPage chooses whether to render a page, redirect or return 404. For an SSG-only application, return 404 for URLs outside its generated route set. Rendering those URLs on request makes the deployment hybrid.

Foldkit generates the shared client/server build identity automatically.

Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:

alchemy.run.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
export const Website = Cloudflare.Website.Foldkit("Foldkit");
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;

WebsiteEnv is the typed shape of the Worker’s bindings. A client-only app with a custom Worker imports it in Custom Worker entry.

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 Website Effect from your Stack and return its URL — see examples/cloudflare-foldkit-spa for the client-only shape and examples/cloudflare-foldkit-ssr for the server-rendered one:

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,
};
}),
);

GET and HEAD requests serve matching client assets and prerendered pages first. The browser build contains a rendered homepage only when / was prerendered; otherwise the homepage reaches the handler. Other application methods reach the Worker rather than a prerendered file.

Browser-only builds use notFoundHandling: "single-page-application". An unmatched path serves index.html, and the browser router resolves the URL. Server builds use normal asset lookup and send misses to Foldkit’s handler.

Explicit assets settings override these defaults. An application that ships a real 404 page opts out of the fallback:

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

Pass browser configuration through VITE_-prefixed keys in env. Alchemy inlines these values into import.meta.env.* during the Vite build:

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

Type a custom public key 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. See Environment for the full inlining semantics.

The same env prop carries runtime bindings, including KV namespaces, secrets and other Workers. A custom Worker reads them from its env argument. Keep secrets out of VITE_* keys and serialized Foldkit Flags; both reach the browser.

A client-only app that must also do something at the edge — serve an API route, wrap the app in error reporting, export Durable Object classes — points main at its own module. The module builds through Vite’s ssr environment and becomes the deployed Worker entry. The client build is still reachable through the Worker’s ASSETS binding, so the Worker answers its own routes and hands everything else back to the asset layer:

src/worker.ts
import type { WebsiteEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WebsiteEnv): 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
export const Ticker = Cloudflare.KV.Namespace("Ticker");
export const Website = Cloudflare.Website.Foldkit("Foldkit", {
main: "src/worker.ts",
env: { TICKER: Ticker },
assets: {
runWorkerFirst: ["/api/*"],
},
});

runWorkerFirst sends /api/* to the Worker ahead of the asset layer. The client-only fallback is still derived — a deep link the Worker hands back through env.ASSETS.fetch is answered with the template as before.

env.TICKER is typed as a KV namespace, and Alchemy supplies the ASSETS binding. These runtime bindings are available to Worker code; browser code receives only the public build-time values.

A server-rendered or prerendered app cannot take a main: with ssr.build set, Foldkit’s plugin owns the server entry, and the build fails if another module claims it. Handle page redirects, status and response headers in renderPage. Put data APIs or additional Worker handlers in a separate Worker.

Terminal window
bun alchemy dev

alchemy dev runs Foldkit’s native Vite dev server with state-preserving live reload and devtools. SSR uses the same server entry as the production build. A development reload restores the Model and rebuilds the DOM; refresh manually to exercise hydration from the server-rendered HTML.

A client-only app with a custom main runs its Worker through workerd and can use Cloudflare bindings. The native Foldkit SSR dev host runs in Node; it does not expose the generated Worker’s bindings as an env argument to renderPage.

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 },
});