# A browser send follows one path to the capability

> For agents: start with the [agent guide](https://ratstack.sh/llms.txt). Every page is Markdown by default; add `Accept: text/html` for HTML.

New on /news: [tla-ts 0.0.9 adds TLC's -continue](/news).

**Goal:** trace one user action from a click to the server and back, and know which file owns each step.

Foldkit 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 capability](/lore/one-capability-every-surface).

## 1. The view sends a Message

The Search button sends `SubmittedSearch`. The view does no fetching.

apps/web/src/features/app.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 224-229; highlighted 226. Title: A click is a Message.

```typescript
          Button.view(
            {
              onClick: Message.SubmittedSearch(),
              toView: ({ button }) => h.button(button, ["Search"]),
              type: "button",
            },
```

Every Message the program accepts is declared in one union:

apps/web/src/client/docs/message.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 7-22; highlighted 17-18,20. Title: SubmittedSearch, SucceededSearch, FailedSearch.

```typescript
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:

apps/web/src/features/app.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 35-42; highlighted 39-40. Title: Loading now; the search runs as a Command.

```typescript
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

apps/web/src/client/docs/command.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 10-18; highlighted 13-15. Title: SearchDocs ends in a result Message.

```typescript
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](/skills/learn-rat-stack) [client](/skills/uncomplect) 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 [handler](/systems/capabilities). [AGENTS.md](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/AGENTS.md)

apps/web/src/client/docs/queries.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 7-11; highlighted none. Title: The client group comes from the contracts.

```typescript
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:

apps/web/src/client/docs/queries.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 16-20; highlighted 18-19. Title: A 30-second cache in front of the search RPC.

```typescript
  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:

apps/site/src/capabilities/search.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 7-11; highlighted none. Title: The server stays authoritative.

```typescript
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

apps/web/src/features/app.ts at a3f5a14861ce89913e82bea0f7ba02c83e8d4725; lines 141-146; highlighted 143. Title: Only the latest search may land.

```typescript
    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.
- `update` is 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 [Schema](/lore/schemas-define-the-boundary) 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](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/client/docs/model.ts)

## Next

Read [UI behaviour is a scene](/lore/ui-behaviour-is-a-scene) to test this program without a browser.
Then read [One capability, every surface](/lore/one-capability-every-surface) for the server half of the contract.

[CAFE](/lore/cafe-stack) names where Foldkit sits in the stack.

## Sources

1. [rat-stack AGENTS.md at a3f5a14](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/AGENTS.md)
   GitHub joelhooks/rat-stack. Web feature blueprint and the no-browser-server-imports boundary. Accessed 2026-11-08.

2. [Docs browser Messages](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/client/docs/message.ts)
   GitHub joelhooks/rat-stack. defineMessageUnion lists every event the docs browser handles. Accessed 2026-11-08.

3. [Docs browser Model](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/client/docs/model.ts)
   GitHub joelhooks/rat-stack. SearchState is Idle, Loading, Loaded or Failed; the Model holds a generation counter. Accessed 2026-11-08.

4. [Docs browser Commands](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/client/docs/command.ts)
   GitHub joelhooks/rat-stack. SearchDocs calls DocumentQueries and maps the result to SucceededSearch or FailedSearch. Accessed 2026-11-08.

5. [Docs browser RPC client](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/client/docs/queries.ts)
   GitHub joelhooks/rat-stack. toRpcGroup builds the client group from searchContract and readContract; the protocol posts to /rpc. Accessed 2026-11-08.

6. [Docs browser update and view](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/web/src/features/app.ts)
   GitHub joelhooks/rat-stack. SubmittedSearch returns SearchDocs; SucceededSearch applies only when its generation matches. Accessed 2026-11-08.

7. [search capability](https://github.com/joelhooks/rat-stack/blob/a3f5a14861ce89913e82bea0f7ba02c83e8d4725/apps/site/src/capabilities/search.ts)
   GitHub joelhooks/rat-stack. implement binds searchContract to the content store on the server. Accessed 2026-11-08.

## Lore on this page

- [CAFE names the Cloudflare, Alchemy, Foldkit and Effect stack](/lore/cafe-stack)

## Linked from

- Agent guide → An Effect stack so pure (aspirational) Kit Langton will blush. → [Read page](https://ratstack.sh/llms.txt)

- Change log → What changed in the files served here, newest first. → [Read page](https://ratstack.sh/log)

- Full agent guide → An Effect stack so pure (aspirational) Kit Langton will blush. → [Read page](https://ratstack.sh/llms-full.txt)

- One capability, every surface → A schema-typed contract and one handler feed every interface rat-stack keeps. → [Read page](https://ratstack.sh/lore/one-capability-every-surface)

- Rat Stack lore | rat-stack → Short, source-grounded notes on the ideas and decisions behind rat-stack. → [Read page](https://ratstack.sh/lore)

- Source change history (log.md) → The generated change log as Markdown. → [Read page](https://ratstack.sh/log.md)

## Unlinked mentions

- [Agent rules for rat-stack (AGENTS.md)](https://ratstack.sh/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)](https://ratstack.sh/README.md) → Foldkit 0.170.0 + @foldkit/vite-plugin 0.29.0: the web UI.

- [CAFE all stars](https://ratstack.sh/systems/all-stars) → Public work builds on or teaches Cloudflare, Alchemy, Foldkit or Effect.¶

- [CAFE names the Cloudflare, Alchemy, Foldkit and Effect stack](https://ratstack.sh/lore/cafe-stack) → Part: Foldkit¶

- [Dependency vendoring rules (vendor/README.md)](https://ratstack.sh/vendor/README.md) → uuidV4() in effect/crypto, and Foldkit 0.170.0 peers on effect \~4.1.0.

- [Effect + Alchemy peer patterns](https://ratstack.sh/resources/same-version-repos) → Does the shared catalog satisfy Foldkit’s exact Effect peers?

- [Effect + Alchemy peers](https://ratstack.sh/resources/peers) → 4 still differs from Foldkit’s exact Effect 4.0.0 peers.

- [Motion](https://ratstack.sh/systems/motion) → The Foldkit adapter, @rat-stack/motion/foldkit, keeps a clock and the named tweens and springs in the Model.

- [rat-stack](https://ratstack.sh/) → web: shared reader models and browser UI built with Foldkit.

- [UI behaviour is a scene](https://ratstack.sh/lore/ui-behaviour-is-a-scene) → Devin Jameson shared a Scene test for the Foldkit UI Toast.

- [Web](https://ratstack.sh/systems/web) → Foldkit is a UI framework on Effect: a Model, Messages, an update function, and a pure view.¶
