A browser send follows one path to the capability
Goal: trace one user action from a click to the server and back, and know which file owns each step.¶
Foldkit27 is the browser UI framework in rat-stack. A Foldkit program keeps its state in a Model. Events arrive as Messages.¶
An update function takes the Model and a Message, and returns a new Model plus any Commands. A Command is a named task that runs outside update and ends by sending another Message.¶
The docs browser at /search is the worked example. Its search box calls the search capability21.¶
1. The view sends a Message
The Search button sends SubmittedSearch. The view does no fetching.¶
Button.view(
{
onClick: Message.SubmittedSearch(),
toView: ({ button }) => h.button(button, ["Search"]),
type: "button",
},Every Message the program accepts is declared in one union:¶
export const Message = defineMessageUnion({
ChangedUrl: { url: Url },
ClickedLink: { request: UrlRequest },
CompletedLoadExternal: {},
CompletedNavigate: {},
FailedRead: {
generation: Schema.Finite,
message: Schema.String,
notFound: Schema.Boolean,
},
FailedSearch: { generation: Schema.Finite },
SubmittedSearch: {},
SucceededRead: { document: ReadOutput, generation: Schema.Finite },
SucceededSearch: { generation: Schema.Finite, result: SearchOutput },
UpdatedQuery: { value: Schema.String },
});2. update returns a Command
update sets the search to Loading, increases a generation counter, and returns the SearchDocs Command:¶
const searchFor = (model: AppModel, query: string): DocsReturn => {
const generation = model.generation + 1;
return {
commands: [SearchDocs({ generation, query })],
model: { ...model, generation, query, search: SearchState.Loading() },
};
};update does no I/O. It describes the next Model and the work to run.¶
3. The Command calls the capability over RPC
export const SearchDocs = Command.define("SearchDocs", {
args: { generation: Schema.Finite, query: Schema.String },
execute: ({ query, generation }) =>
DocumentQueries.use((queries) => queries.search(query)).pipe(
Effect.map((result) => Message.SucceededSearch({ generation, result })),
Effect.orElseSucceed(() => Message.FailedSearch({ generation }))
),
messages: [Message.SucceededSearch, Message.FailedSearch],
});DocumentQueries builds its RPC client from the shared contracts. The rat-stack-boundaries/no-browser-server-imports lint rule lets browser modules import @rat-stack/core/contracts and @rat-stack/capability/rpc-group, never a handler28. AGENTS.md¶
const { group } = toRpcGroup([searchContract, readContract]);
const protocol = RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson])
);DocumentQueries keeps each search result for 30 seconds. A repeated query in that window is answered from the cache, with no fresh RPC. The RPC call runs on a cache miss:¶
const search = yield* Cache.make({
capacity: Number.POSITIVE_INFINITY,
lookup: (query: string) => client.search({ limit: 10, query }),
timeToLive: "30 seconds",
});On the server, implement binds the same searchContract to the content store:¶
export const search = implement(searchContract, ({ limit, query }) =>
ContentStore.use((store) => store.search(query, limit)).pipe(
Effect.map((matches) => ({ matches, total: matches.length }))
)
);4. update folds the result into the Model
SucceededSearch: ({ generation, result }) => ({
model:
generation === model.generation
? { ...model, search: SearchState.Loaded({ result }) }
: model,
}),The generation check drops a stale reply. If the user searches twice, the first reply carries an old generation, and update keeps the Model unchanged.¶
Why one path
Each step has one owner, so each step can be tested alone.¶
- The Model is data. The view reads it and sends Messages.¶
updateis a function of the Model and a Message. It runs without a network.¶- In this program, only Commands call
DocumentQueries, the RPC client. Each Command ends in a Message.¶ - The contract is the one Schema30 set that the browser and the server share.¶
SearchState names four states: Idle, Loading, Loaded, and Failed. The view matches on all four, so no flag combination can show a spinner and an error at once. model.ts¶
Next
Read UI behaviour is a scene8 to test this program without a browser. Then read One capability, every surface21 for the server half of the contract.¶
CAFE27 names where Foldkit sits in the stack.¶
Sources
- rat-stack AGENTS.md at a3f5a14. GitHub joelhooks/rat-stack. Web feature blueprint and the no-browser-server-imports boundary. Accessed 2026-11-08.
- Docs browser Messages. GitHub joelhooks/rat-stack. defineMessageUnion lists every event the docs browser handles. Accessed 2026-11-08.
- Docs browser Model. GitHub joelhooks/rat-stack. SearchState is Idle, Loading, Loaded or Failed; the Model holds a generation counter. Accessed 2026-11-08.
- Docs browser Commands. GitHub joelhooks/rat-stack. SearchDocs calls DocumentQueries and maps the result to SucceededSearch or FailedSearch. Accessed 2026-11-08.
- Docs browser RPC client. GitHub joelhooks/rat-stack. toRpcGroup builds the client group from searchContract and readContract; the protocol posts to /rpc. Accessed 2026-11-08.
- Docs browser update and view. GitHub joelhooks/rat-stack. SubmittedSearch returns SearchDocs; SucceededSearch applies only when its generation matches. Accessed 2026-11-08.
- search capability. GitHub joelhooks/rat-stack. implement binds searchContract to the content store on the server. Accessed 2026-11-08.
Last changed 2026-11-08 in b8f4ccf. Source on GitHub. Change log.
Linked from
- Agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
Link context
A browser send follows one path to the capability — In the Foldkit docs browser, a search is a Message, then an update, then a named Command that calls the search capability over RPC on a cache miss and returns a result Message.
- Change log. What changed in the files served here, newest first.
A browser send follows one path to the capability — new page: Initial commit from joelhooks/rat-stack template
- Full agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
- One capability, every surface. A schema-typed contract and one handler feed every interface rat-stack keeps.
Read A browser send follows one path to the capability to see search called over RPC.¶
- Rat Stack lore | rat-stack. Short, source-grounded notes on the ideas and decisions behind rat-stack.
Link context
A browser send follows one path to the capability — In the Foldkit docs browser, a search is a Message, then an update, then a named Command that calls the search capability over RPC on a cache miss and returns a result Message.
- Source change history (log.md). The generated change log as Markdown.
A browser send follows one path to the capability — new page: Initial commit from joelhooks/rat-stack template
Unlinked mentions (11)
- Agent rules for rat-stack (AGENTS.md) → Foldkit 0.170.0 with @foldkit/ui and @foldkit/devtools 0.170.0, @foldkit/markdown 0.15.0, and @foldkit/vite-plugin 0.29.0 for the web UI, server-rendered and prerendered by default
- Build and run rat-stack (README.md) → Foldkit 0.170.0 + @foldkit/vite-plugin 0.29.0: the web UI.
- CAFE all stars → Public work builds on or teaches Cloudflare, Alchemy, Foldkit or Effect.¶
- CAFE names the Cloudflare, Alchemy, Foldkit and Effect stack → Part: Foldkit¶
- Dependency vendoring rules (vendor/README.md) → uuidV4() in effect/crypto, and Foldkit 0.170.0 peers on effect ~4.1.0.
- Effect + Alchemy peer patterns → Does the shared catalog satisfy Foldkit’s exact Effect peers?
- Effect + Alchemy peers → 4 still differs from Foldkit’s exact Effect 4.0.0 peers.
- Motion → The Foldkit adapter, @rat-stack/motion/foldkit, keeps a clock and the named tweens and springs in the Model.
- rat-stack → web: shared reader models and browser UI built with Foldkit.
- UI behaviour is a scene → Devin Jameson shared a Scene test for the Foldkit UI Toast.
- Web → Foldkit is a UI framework on Effect: a Model, Messages, an update function, and a pure view.¶