# In the Browser

Run a nanocode session in a web page, with sqlite-wasm for storage and a server-side proxy that holds the model credentials.

## Overview

The module runs in any browser that instantiates WebAssembly. A page compiles it, stores the session in an in-memory sqlite-wasm database, and sends model requests to its own server. The server adds the credentials, so no API key reaches the page.

This site's home page runs this setup. It opens a Codex session against `/api/openai`, and the site's Worker forwards each request to Cloudflare AI Gateway.

```ts
import wasm from 'nanocode-wasm/nanocode_wasm.wasm?url'

// Compile the module while the page downloads it
const module = await WebAssembly.compileStreaming(fetch(wasm))
```

## Walkthrough

The steps below build `agent.ts`, a browser module that opens a session, streams assistant text, and runs a turn. The build uses Vite.

### Configure Vite

Build and link `nanocode-wasm` as [Getting Started](/docs/introduction/getting-started) shows, then install `@sqlite.org/sqlite-wasm`. Exclude sqlite-wasm from dependency prebundling, because the package locates its own `.wasm` file.

```ts title="vite.config.ts"
import { defineConfig } from 'vite'

export default defineConfig({
  // Prebundling moves sqlite-wasm away from its .wasm file
  optimizeDeps: { exclude: ['@sqlite.org/sqlite-wasm'] },
})
```

### Load Both Modules

The `?url` suffix makes Vite emit the nanocode module as an asset and import its URL. `WebAssembly.compileStreaming` compiles the module as it downloads. Compile once per page, since every session on the page uses the same bytes.

```ts title="agent.ts"
import sqlite3InitModule from '@sqlite.org/sqlite-wasm'
import wasm from 'nanocode-wasm/nanocode_wasm.wasm?url'

// 1. Compile nanocode and start SQLite at the same time
const [module, sqlite3] = await Promise.all([
  WebAssembly.compileStreaming(fetch(wasm)),
  sqlite3InitModule(),
])

// 2. An in-memory database, which lasts as long as the page
const database = new sqlite3.oo1.DB(':memory:', 'c')
```

The server must serve `.wasm` files as `application/wasm`, which `compileStreaming` requires.

### Store the Session

The `SqlHost` runs each SDK transaction against the database. sqlite-wasm's `exec` returns rows as arrays with `rowMode: 'array'`.

```ts title="agent.ts"
import type { SqlHost, SqlValue } from 'nanocode-wasm'

// Runs each SDK transaction all or nothing
const sql: SqlHost = {
  transaction(_kind, _operation, statements) {
    database.exec('BEGIN IMMEDIATE')
    try {
      const rows = statements.map(
        ([query, ...bind]) =>
          database.exec({
            bind: bind as never,
            returnValue: 'resultRows',
            rowMode: 'array',
            sql: query,
          }) as SqlValue[][],
      )
      database.exec('COMMIT')
      return rows
    } catch (error) {
      database.exec('ROLLBACK')
      throw error
    }
  },
}
```

### Stream Events

`published` receives each batch of logged events, in sequence order, after the batch is committed. An agent event has `source` set to `agent` and carries the SDK event in `event`, with its `type` and `payload`.

```ts title="agent.ts"
import { AgentRuntime, type LoggedEvent } from 'nanocode-wasm'

type Logged = {
  event?: { payload?: Record<string, unknown>; type?: string }
  source?: string
}

const output = document.querySelector('output')!

// Appends assistant text and shows tool calls as they are logged
function render(entry: LoggedEvent) {
  const logged = entry.event as Logged
  if (logged.source !== 'agent' || !logged.event) return
  const { payload = {}, type } = logged.event
  if (type === 'assistant.delta')
    output.append(String(payload.text ?? ''))
  if (type === 'tool.call') console.log(payload.tool, payload.arguments)
}

const runtime = await AgentRuntime.instantiate(module, sql, {
  published: (events) => events.forEach(render),
})
```

These event types drive a chat view:

* **`assistant.delta`:** A piece of assistant text in `payload.text`, for the output item in `payload.item_id`.
* **`assistant.message`:** The complete text of one assistant message in `payload.text`.
* **`tool.call`:** A tool call, with the tool name in `payload.tool` and its input in `payload.arguments`.
* **`tool.result`:** The tool's output in `payload.result`, with a `status` of `completed` or `failed`.

### Open the Session

Point `endpoint` at the page's own server. `dispatch` forwards the request with `fetch` and adds no credentials. `event_coalesce_ms` merges consecutive deltas logged within the window into one event, which reduces the events the page renders.

```ts title="agent.ts"
import { sessionUuid } from 'nanocode-wasm'

// 1. A Codex session whose requests go to /api/openai/responses on this
// origin
await runtime.open(
  {
    endpoint: `${location.origin}/api/openai`,
    event_coalesce_ms: 50,
    model: 'gpt-6-luna',
    provider: 'codex',
    scope: 'page',
    session_id: sessionUuid(),
    subscription: false,
    thinking: 'low',
  },
  {
    // 2. The server holds the credentials, so the request goes out
    // unchanged
    dispatch: (request, signal) =>
      fetch(request.endpoint, {
        body: request.body as Uint8Array<ArrayBuffer>,
        headers: request.headers,
        method: 'POST',
        redirect: 'error',
        signal,
      }),
  },
)
```

### Add the Proxy

The proxy accepts the SDK's request, checks it, adds the credentials, and streams the provider's response back. The shape below is a Worker route that calls the OpenAI Responses API with a secret.

```ts title="worker.ts"
// Forwards POST /api/openai/responses with the server's API key
export default {
  async fetch(
    request: Request,
    env: { OPENAI_API_KEY: string },
  ): Promise<Response> {
    const url = new URL(request.url)
    if (
      url.pathname !== '/api/openai/responses' ||
      request.method !== 'POST'
    )
      return new Response('Not found', { status: 404 })

    // 1. Refuse models the server does not serve
    const body = await request.text()
    if (JSON.parse(body).model !== 'gpt-6-luna')
      return new Response('Model refused', { status: 400 })

    // 2. Add the credential and stream the response back
    return fetch('https://api.openai.com/v1/responses', {
      body,
      headers: {
        accept: 'text/event-stream',
        authorization: `Bearer ${env.OPENAI_API_KEY}`,
        'content-type': 'application/json',
      },
      method: 'POST',
      signal: request.signal,
    })
  },
}
```

This site's proxy, `src/Model.ts`, works the same way with more checks. It refuses any model or reasoning effort other than the demo's, rate limits each address, limits the request size, and calls AI Gateway through the Worker's `AI` binding instead of `fetch`.

> [!NOTE]
> The proxy above is a general shape, not code from this site. Copy the headers that your provider expects, and keep request checks on the server, since the page controls the request body.

### Run a Turn

Submit the prompt, then drive queued turns until none remain. The deltas reach `render` while `drive` runs.

```ts title="agent.ts"
// 1. Queue the prompt
const request_id = crypto.randomUUID()
await runtime.submit('page', {
  metadata: {},
  prompt: 'Explain WebAssembly in one sentence.',
  request_id,
})

// 2. Drive turns. A waiting turn retries its model request after a
// delay.
for (;;) {
  const next = (await runtime.pending('page'))[0]
  if (!next) break
  const driven = await runtime.drive(next.report.request_id)
  if (driven.retry)
    throw new Error(
      'The session must reopen. Reload the page to start again.',
    )
  if (driven.waiting)
    await new Promise((resolve) => setTimeout(resolve, 500))
}
```

## Recipes

### Offer Page Tools

Host tools run in the page, so the model can read or change page state. Declare them in `config.tools` and run them in `tools.call`.

```ts
// The model can read the page title
await runtime.open(
  {
    ...config,
    tools: [
      {
        description: 'Read the page title.',
        name: 'title',
        parameters: { type: 'object' },
      },
    ],
  },
  {
    dispatch,
    tools: { call: async () => ({ title: document.title }) },
  },
)
```

### Count Requests

`runtime.metrics` counts model requests and other host calls, which a page can show next to the chat.

```ts
// Model requests since the instance started
const requests = runtime.metrics.model_requests
```

### Close on Unload

`dispose` shuts the SDK session down and cancels open model requests. Call it before the page discards the runtime.

```ts
// Releases the session when the page goes away
addEventListener('pagehide', () => void runtime.dispose())
```

## More

- [Getting Started](/docs/introduction/getting-started): Build the package and drive a turn from Node.

- [Tools](/docs/guides/tools): Results, errors, and parallel calls.

- [Events](/docs/guides/events): The agent events a session logs.

- [Model Gateways](/docs/guides/gateways): Send model requests through a gateway.

- [instantiate](/docs/api/wasm/instantiate): The published and wake host options.

- [Types](/docs/api/wasm/types): LoggedEvent and the other package types.
