Similar to how Rust is obviously a better choice today than C++. Who cares if the language is “more difficult” to code in, if the agents are doing the coding. What we need is building blocks that make it harder to author bugs.
Especially after having used ruby and elixir extensively after years of react/Vue, it feels so backwards (and LLM unfriendly) to split front and backend unless you have gargantuan reactivity needs (you don't).
I wish JS offered just one proper server side focused frontend solution.
a) Actually ship docs, and use AI to help (I also do a ton of hand editing and review cycles)
b) Have poor docs coverage because I’m handwriting everything
I’ve chosen A, but plan to do a few weeks of docs rewrites before I ship 1.0.0. I would rather it sound like me.
Cons: The API surface is huge so there is a steep learning curve.
IMO the reset should fire regardless what happens after it.
Perhaps it makes sense for AI agents, but as a human, I will pass.
but my experience working with elm is that things just work almost always the first time
if they can bring that over, many will take that trade off
yes somethings are harder to express, but its subjective if its a bad thing given the correctness guarantees
i haven't firmly run into the perf ceiling myself, but yes it is obviously there for more interactive apps
The learning curve is steep tho.
In any case it's a hard technology to sell, you don't appreciate it from hello worlds. In fact you hate it for trivial programs.
It shows it's benefits at scale.
Lots of tools like T3 and opencode use it at their core because they solve non trivial problems made of queues, retries, dependency injection, schemas, etc.
I've written a vscode extension with it:
https://github.com/dearhuman/effect-decorate/blob/main/src/e...
These days? Doing this FE SPA framework for Effect/typescript familiarity or Fable for F# familiarity and a model-view update pattern can help one do CQRS better, force you to think about stronger Hexagonal Architecture patterns, and overall help your application care about long term correctness more without you having to hand code every discriminated union as such.
The LLM can't replace you, but it can be an enzyme that lowers the activation energy required to switch from bad old MVC mutable tag soup to MVU frontends with strong data interface designs held in sqlite-dbs-per-aggregate (if you are of a DDD or event sourcing taste, which this pattern does not require!)
Which I don't expect from the description, and I also don't expect from the code.
---
So outside of the simple "This is broken..." feedback, I want to further pick on this example:
Don't fucking hide the imports.
Especially don't fucking hide the imports if you're showing example code, and you're doing things like
```
import { Match as M, Schema as S } from 'effect'
import { m } from 'foldkit/message'
```
Which I only know because I bothered to dig through the example playground counter (which is a different example entirely!)
It's a huge issue to show demo code where concepts magically appear, and it's just bad manners to use shorthand at the same time.
- Elm CSS and even then, it was on a page with I think hundreds of accordions. We had to use Css.Global to create class based states, then toggle classes.
- Loading a massive library of data and leaving it in the state to make navigation within that library's pages faster. It was a classic tradeoff of upfront vs incremental load time, and we'd just over indexed in one direction.
Beta
Built on Effect. Architected like Elm. Written in TypeScript.
npx create-foldkit-app@latest
React, Vue, Svelte, and Solid solve rendering and leave the architecture to you. Foldkit gives you the architecture, so you can focus on your domain.
One immutable model holds your entire application state. Every change flows through a single update function. No hidden mutations, no stale closures, no surprises.
Side effects are values you return from update, not imperative calls buried in handlers. Commands describe what should happen. The runtime handles when and how.
Complexity grows linearly, not exponentially. A 50-file app follows the same patterns as a 5-file app, so each feature adds structure instead of entangling with what is already there. New team members read the code and understand it.
If you already know Effect, Foldkit feels natural. If you’re new to Effect, Foldkit is a great way to learn it.
Watch a message flow through update into the model. The code highlights in real time to show you what’s happening at each step.
// MODEL
const Model = S.Struct({
count: S.Number,
isResetting: S.Boolean,
resetDuration: S.Number,
})
// MESSAGE
const ClickedIncrement = m('ClickedIncrement')
const ChangedResetDuration = m('ChangedResetDuration', {
seconds: S.Number,
})
const ClickedResetAfterDelay = m('ClickedResetAfterDelay')
const CompletedDelayReset = m('CompletedDelayReset')
// COMMAND
const DelayReset = Command.define(
'DelayReset',
{ seconds: S.Number },
CompletedDelayReset,
)(({ seconds }) =>
Effect.sleep(`${seconds} seconds`).pipe(
Effect.as(CompletedDelayReset()),
),
)
// UPDATE
M.tagsExhaustive({
ClickedIncrement: () => [
evo(model, { count: count => count + 1 }),
[],
],
ChangedResetDuration: ({ seconds }) => [
evo(model, { resetDuration: () => seconds }),
[],
],
ClickedResetAfterDelay: () => [
evo(model, { isResetting: () => true }),
[DelayReset({ seconds: model.resetDuration })],
],
CompletedDelayReset: () => [
evo(model, { count: () => 0, isResetting: () => false }),
[],
],
})
0
Reset Delay (seconds)
Model State
count: 0
isResetting: false
resetDuration: 2
Phase
Idle
Message Log
Most frameworks ask you to bring your own routing library, state manager, UI kit, and form validator. Foldkit ships them as one coherent system.
Type-safe bidirectional routing. URLs parse into typed routes and routes build back into URLs. No string matching, no mismatches between parsing and building.
Accessible components (dialog, menu, tabs, listbox, disclosure, and more) built for The Elm Architecture. Easy to style and customize.
A self-contained Model, Messages, update, and view, embedded inside a larger program. Children surface domain facts as typed OutMessages and parents handle them in update. Every stateful Foldkit UI component ships as a Submodel.
Bind a slice of the Model to a scoped Stream that may emit Messages. The runtime opens the scope while the slice holds its value and closes it when the slice changes.
Model-driven lifecycles for long-lived browser resources like WebSockets, AudioContext, and RTCPeerConnection. The runtime acquires when the Model calls for the resource and releases when it no longer does.
Per-field validation with sync and async support. Define rules as predicates, apply them in update, and the Model tracks every field state.
Two test primitives. Story sends Messages through update and asserts on the resulting Model and Commands. Scene drives the rendered view through accessible locators and asserts on the re-rendered HTML.
Inspect Messages, Model state, and Commands as your app runs. Time-travel mode rewinds your UI to any past Model. AI agents can connect to the same data over MCP.
Run a Foldkit widget inside any host application with Runtime.embed. The host pushes data in and receives values out through Schema-typed Ports, and tears the widget down with dispose.
Pure update functions mean pure tests. Story tests the state machine. Scene tests features through the view (clicking buttons, typing into inputs) with accessible locators. No DOM, no mocking.
import { Scene, Story } from 'foldkit'
import { expect, test } from 'vitest'
// Story — test the state machine
test('fetch weather updates the model', () => {
Story.story(
update,
Story.with(model),
Story.message(SubmittedWeatherForm()),
Story.model(model => {
expect(model.weather._tag).toBe('WeatherLoading')
}),
Story.Command.expectExact(FetchWeather),
Story.Command.resolve(FetchWeather, SucceededFetchWeather({ weather })),
Story.model(model => {
expect(model.weather._tag).toBe('WeatherSuccess')
}),
)
})
// Scene — test through the view
test('type a zip code, click get weather, see the forecast', () => {
Scene.scene(
{ update, view },
Scene.with(model),
Scene.type(Scene.label('Zip code'), '90210'),
Scene.click(Scene.role('button', { name: 'Get Weather' })),
Scene.expect(Scene.role('button', { name: 'Loading...' })).toExist(),
Scene.Command.expectExact(FetchWeather),
Scene.Command.resolve(FetchWeather, SucceededFetchWeather({ weather })),
Scene.inside(
Scene.role('article'),
Scene.expect(Scene.text('Beverly Hills, California')).toExist(),
Scene.expect(Scene.text('72°F')).toExist(),
),
)
})
When every state change flows through Messages and a single Model, you get DevTools that would be impossible in a mutable-state framework. Every Message is logged. Every Model state is inspectable. Click any row to see exactly what changed.
Plus, AI agents can connect over MCP. They read the current Model, walk Message history, and rewind the UI to past states. Programmatic access to the same data DevTools shows you.
This site runs on Foldkit. Look for the tab on the bottom right of this page to try DevTools live.

Foldkit apps are explicit and predictable. This makes LLMs particularly good at generating Foldkit code. And it makes generated Foldkit code exceptionally easy for humans to review.
AI agents can also connect directly to a running Foldkit app over the Model Context Protocol. They read the current Model, inspect Message history, rewind the UI to past states, and dispatch Messages.
Set up AI-assisted development
Foldkit asks you to think about frontend development differently. It uses The Elm Architecture, so there are no components, no hooks, no local state. Everything is declarative and structured. You’ll need to shift how you think about state, effects, and views.
It’s a discipline. It pays off, but it’s a real ask.
Foldkit is a different kind of frontend framework. If you’re weighing it against React, Vue, Svelte, or Solid, the key difference isn’t syntax or performance. It’s that Foldkit prescribes the architecture instead of leaving it to you.
Your backend already uses Effect. Foldkit is the missing frontend piece: same ecosystem, same patterns, no context switching.
You want your architecture to prevent bugs, not just catch them.
One pattern for state, effects, and views means less disagreement and faster onboarding.
Auth flows, real-time data, multi-step forms. Each becomes explicit state and transitions, so the hard parts stay readable instead of hiding in effects and refs.
Foldkit isn’t an incremental adoption. It’s a different architecture, and migrating means a rewrite. The middle path is embedding: Runtime.embed runs a Foldkit widget inside an existing app.
Foldkit leans on pipe, discriminated unions, and Effect throughout. There’s no escape hatch. You’re all in or you’re not.
No React component libraries, no Next.js, no existing middleware. You’re building on different foundations.
Foldkit is a client-side SPA framework. Static generation is possible, but you’ll roll your own (like we do for this website).
New releases, patterns, and the occasional deep dive.