Skip to content

Foldkit

Fly.Website.Foldkit deploys a Foldkit app to Fly. It supports browser-only SPAs, server-side rendering (SSR), static site generation (SSG), and hybrid rendering. Foldkit’s Vite plugin generates the handler and assets; Alchemy packages them for deployment.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads its /foldkit and /foldkit/node exports from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
pnpm add -D @alchemy.run/frontend-frameworks@latest

Your Vite config stays what Foldkit’s setup gives you. Vite plugins like Tailwind go in plugins as usual:

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

Alchemy runs Vite programmatically on the project root. The Foldkit plugin and the rest of your setup are preserved as-is.

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 client assets and generated handler. GET and HEAD requests serve matching assets and prerendered pages first; remaining requests go to the handler. Other methods bypass static assets. The application owns its response status and headers through renderPage. For an SSG-only application, return 404 for URLs outside its generated route set; allowing those URLs to render on request makes the deployment hybrid.

Browser-only builds use SPA fallback. Server builds send unmatched requests to the handler. Explicit assets.notFoundHandling settings override these defaults.

Vite’s resolved base also mounts the client assets. With base: "/app/", /app/assets/main.js serves assets/main.js from the client output. SPA fallback is limited to that prefix; server handlers receive the original URL.

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as Fly from "@alchemy.run/fly";
export const Website = Fly.Website.Foldkit("Website");

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

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

Yield the site from your Stack and return its URL:

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

site.url is https://{app}.fly.dev. The site also exposes app, service, ip, and certificate — they are undefined under alchemy dev.

Set process environment variables through the top-level env map. Server code reads private values from process.env; Vite inlines VITE_* values into the browser build. Values must be strings or Redacted strings.

alchemy.run.ts
export const Website = Fly.Website.Foldkit("Website", {
env: {
API_BASE: "https://api.example.com",
VITE_API_URL: "https://api.example.com",
},
});

The values are copied onto process.env before Vite dev and build, and onto the Machine. Server code reads process.env.API_BASE. Client code reads import.meta.env.VITE_API_URL. Keep secrets out of VITE_* keys and serialized Foldkit Flags.

Type a custom public key with Vite’s standard environment declaration:

src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
const apiUrl = import.meta.env.VITE_API_URL;

A browser-only Foldkit app that uses URL routing (Runtime.makeApplication with routing, onUrlRequest, and onUrlChange) resolves routes on the client, so a deep link like /counter/42 arrives as a request for a file that doesn’t exist.

For browser-only builds, Foldkit defaults assets.notFoundHandling to "single-page-application", which returns index.html (200) for unmatched paths so the Foldkit runtime can resolve the route. Use "404-page" to serve a real 404 instead:

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

alchemy dev runs Vite’s own dev server (Foldkit’s live reload and devtools) instead of deploying; site.url is the local address and no Fly App, Service, IP, or certificate is created. Wrap the site in Alchemy.remote() to deploy the live Service even during dev.

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

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

Pass a hostname to request ACME (Fly.Certificate) on the App. url becomes https://{domain}. v1 expects that DNS already points at the App.

const site = yield* Fly.Website.Foldkit("Web", {
domain: "app.example.com",
});