
Foldkit Has Server Rendering
August 15, 2026 · Devin Jameson
Until today, Foldkit was purely an SPA framework. The server sent an HTML file to the client, the client requested the JavaScript bundle, and once it loaded, the application booted.
There are issues with this approach in isolation:
The user sees the server's HTML before the JavaScript loads. Unless you prerender HTML that matches the loaded app, that is typically a blank page.
There is no first-class way to do SSG. The workaround is a heavy prerender step: for example, using Playwright to visit each route and capture HTML (which the Foldkit website used to do).
There is no first-class way to do request-time SSR: for example, the server sending the client an initial HTML file personalized to the logged-in user.
There was one more problem. Foldkit not having SSR was the deal-breaker for Michael Arnaldi, BDFL of Effect. And that simply will not do.
So, Foldkit can render on the server now. It shipped today under foldkit/experimental/server. It will be promoted to foldkit/server once the community puts it through its paces.
foldkit/experimental/server ships renderToString, and foldkit/runtime ships hydrate. Together, they give a Foldkit application two new ways to reach the browser: generate static HTML for selected routes during the build, or render each request on a server. When the server and client agree, the browser adopts the served HTML in place. A mismatched subtree is rebuilt, and an invalid or cross-deployment handoff is refused. The application that boots is the same Foldkit application you wrote yesterday.
A Foldkit app can remain a pure client-side SPA. Server rendering is opt-in.
SSG and SSR are the same thing run at different times. Either a build script calls your server entry once per URL and writes files, or a server calls it once per request and sends the response. The entry is a small module that derives flags from a Request and asks Foldkit to render:
import { Effect } from 'effect'
import { Server } from 'foldkit/experimental'
import { readCountCookie } from './cookie'
import { Flags, init, view } from './main'
const flagsForRequest = (request: Request): Flags => ({
initialCount: readCountCookie(request.headers.get('cookie') ?? ''),
})
export const renderPage = (request: Request): Promise<Server.EntryResult> =>
Effect.runPromise(
Effect.gen(function* () {
const renderedApplication = yield* Server.renderToString(
{ Flags, init, view },
{
flags: flagsForRequest(request),
buildId: import.meta.env.FOLDKIT_BUILD_ID,
},
)
return Server.Rendered(renderedApplication, {
headers: {
'cache-control': 'private, no-store',
vary: 'cookie',
},
})
}),
)The Flags Schema, init, view, and flagsForRequest are provided by you. The rest is wiring.
The same entry serves Vite in development, a Node host in production, a build script for static generation, and fetch-native runtimes like Cloudflare Workers. Hosts are interchangeable because the entry deals only in Web standards: a Web Request goes in, and the host turns the result into a Web Response.
Foldkit's view is a pure function of the Model, so most of server rendering was less "add SSR to the framework" and more "call the same function somewhere else."
If you are using Foldkit without SSR, the browser runs the whole sequence: your flags Effect produces the flags, init turns them into the first Model, and view renders it.
With SSR, the server runs the front of that sequence and the browser finishes it:
On the server,
flagsForRequestturns the request into flags,initbuilds the first Model, andviewrenders it to HTML. That HTML ships to the browser with the flags serialized alongside it.On the client,
initruns again with those same flags, rebuilds the initial Model, andviewrenders it. Hydration adopts matching server DOM and rebuilds a mismatched subtree from its nearest parent.
There is no separate server-side Model to reconcile with the client's. Both sides derive the initial Model from the same flags and init, then render it through the same view.
After hydration, the initial Commands run, and your Foldkit application behaves like a typical SPA with client-side navigation.
Check out the new docs page on Server Rendering, and the runnable Static Site Generation and Server-Side Rendering examples.
This website (foldkit.dev) now prerenders every route through the same renderPage contract (SSG). The page you are reading hydrated in place.
Foldkit is not getting a second programming model. It is gaining a crucial capability.
There are no server components, no 'use client' or 'use server' boundaries, and no plan for them: the view is one function of one Model. Server rendering changes where the first paint comes from, not how you write applications.
Scaffold a Foldkit SSR application:
npx create-foldkit-app@latest --rendering ssr
Check it out and let me know what breaks. And don't be a stranger!
Devin
