# Cloudflare Workers

Run nanocode sessions in Durable Objects: import the module through Wrangler, store each session in Durable Object SQL, and stream events over Server-Sent Events.

## Overview

A Durable Object fits one session: it has its own SQLite storage and runs single-threaded. The object instantiates `nanocode-wasm` over its storage, drives turns, and serves the logged events to readers. The Worker in front routes requests to the session's object.

Wrangler loads `.wasm` imports as a `WebAssembly.Module`, so the Worker imports the compiled module directly. The package checks against the standard web platform types only, and the host supplies every Cloudflare binding.

```ts
import module from 'nanocode-wasm/nanocode_wasm.wasm'

// The import is already a compiled WebAssembly.Module
const runtime = await AgentRuntime.instantiate(module, sql(ctx), {
  published,
})
```

[gakonst/nanocode-worker](https://github.com/gakonst/nanocode-worker) is the full Cloudflare host. It runs sessions in Durable Objects with hibernation, alarms, egress, and hands that attach over WebSocket. It depends on the package as `file:../nanocode-wasm`.

> [!NOTE]
> The Durable Object code on this page is a sketch built from the nanocode-wasm contracts. It is not taken from nanocode-worker, and this site does not test it. Cloudflare types such as `DurableObjectState` come from `@cloudflare/workers-types`.

## Walkthrough

The steps below build a Worker with one Durable Object class, `Session`, that runs Codex turns and streams their events.

```mermaid
flowchart LR
  client[Client] --> worker[Worker]
  worker --> session[Session object]
  session -->|SqlHost| storage[(Durable Object SQL)]
  session -->|dispatch| gateway[AI Gateway]
  session -->|EventHub.stream| client
```

### Configure Wrangler

Build and link the package as [Getting Started](/docs/introduction/getting-started) shows. Declare the Durable Object with SQLite storage, and store the gateway token as a secret.

```jsonc title="wrangler.jsonc"
{
  // One SQLite-backed Durable Object class holds the sessions
  "compatibility_date": "2026-10-01",
  "durable_objects": { "bindings": [{ "class_name": "Session", "name": "SESSION" }] },
  "main": "src/worker.ts",
  "migrations": [{ "new_sqlite_classes": ["Session"], "tag": "v1" }],
  "name": "agents",
  "vars": { "ACCOUNT_ID": "your-account-id", "GATEWAY_ID": "your-gateway" }
}
```

```bash
# Stores the AI Gateway token outside the configuration
npx wrangler secret put CF_AIG_TOKEN
```

### Store in SQL

Durable Object SQL satisfies `SqlHost` synchronously. `transactionSync` makes one operation's statements atomic, and `raw()` returns rows as arrays.

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

// Runs each SDK operation in one Durable Object transaction
export function sql(ctx: DurableObjectState): SqlHost {
  return {
    transaction(_kind, _operation, statements) {
      return ctx.storage.transactionSync(() =>
        statements.map(
          ([query, ...values]) =>
            [
              ...ctx.storage.sql.exec(query, ...values).raw(),
            ] as SqlValue[][],
        ),
      )
    },
  }
}
```

Set `value_limit` to the largest value that Durable Object SQL stores, from Cloudflare's documented limits. The SDK then keeps every write within it, as [Durability](/docs/guides/durability) describes.

### Open the Runtime

The object instantiates the runtime on first use and keeps it while the object stays in memory. The Worker names each object after its session ID, so a new instance over the same storage continues the stored session.

```ts title="src/session.ts"
import { DurableObject } from 'cloudflare:workers'
import { AgentRuntime, EventHub } from 'nanocode-wasm'
import module from 'nanocode-wasm/nanocode_wasm.wasm'
import { sql } from './sql.js'

type Env = {
  ACCOUNT_ID: string
  CF_AIG_TOKEN: string
  GATEWAY_ID: string
}

const scope = 'owner'

export class Session extends DurableObject<Env> {
  // 1. Keeps at most 128 recent events or 4 MiB of event text for
  // readers
  hub = new EventHub(
    () => 128,
    () => 4 * 1024 * 1024,
  )
  runtime: Promise<AgentRuntime> | undefined

  open(session_id: string): Promise<AgentRuntime> {
    this.runtime ??= (async () => {
      // 2. Every committed batch reaches the hub
      const runtime = await AgentRuntime.instantiate(
        module,
        sql(this.ctx),
        {
          published: (events, bytes) => this.hub.publish(events, bytes),
        },
      )
      // 3. The same session ID on every open continues the stored
      // session
      await runtime.open(
        {
          endpoint: `https://gateway.ai.cloudflare.com/v1/${this.env.ACCOUNT_ID}/${this.env.GATEWAY_ID}/openai`,
          model: 'gpt-6-luna',
          provider: 'codex',
          scope,
          session_id,
          subscription: false,
        },
        {
          dispatch: (request, signal) => this.dispatch(request, signal),
        },
      )
      return runtime
    })()
    return this.runtime
  }
}
```

The SDK requires a UUIDv7 session ID. Create one with `sessionUuid()` when a client starts a session, and route to the object with `env.SESSION.idFromName(id)`.

### Dispatch Requests

The SDK sends no credentials. `dispatch` adds the gateway token, and AI Gateway adds the provider key. Forward the body unchanged, and refuse redirects.

```ts title="src/session.ts"
// Inside the Session class: sends one model request through AI Gateway
dispatch(
  request: {
    body: Uint8Array
    endpoint: string
    headers: [string, string][]
  },
  signal: AbortSignal,
) {
  return fetch(request.endpoint, {
    body: request.body,
    headers: [
      ...request.headers,
      ['cf-aig-authorization', `Bearer ${this.env.CF_AIG_TOKEN}`],
    ],
    method: 'POST',
    redirect: 'error',
    signal,
  })
}
```

To call the provider without a gateway, set `endpoint` to `https://api.openai.com/v1` and add an `authorization` header with the API key instead.

### Run Turns

A `POST` submits a prompt and drives queued turns in the background. `ctx.waitUntil` keeps the object running after the response is sent.

```ts title="src/session.ts"
// Inside the Session class: queues a prompt, then drives it
async submit(session_id: string, prompt: string, request_id: string) {
  const runtime = await this.open(session_id)
  const submitted = await runtime.submit(scope, {
    metadata: {},
    prompt,
    request_id,
  })
  this.ctx.waitUntil(this.drive(runtime))
  return submitted
}

async drive(runtime: AgentRuntime) {
  for (;;) {
    const next = (await runtime.pending(scope))[0]
    if (!next) return
    const driven = await runtime.drive(next.report.request_id)
    // The runtime disposed itself, so the next request opens a new one
    if (driven.retry) return void (this.runtime = undefined)
    if (driven.waiting)
      await new Promise((resolve) => setTimeout(resolve, 1000))
  }
}
```

### Stream Events

`EventHub.stream` serves logged events as Server-Sent Events. It reads from the hub's tail when the tail covers the cursor, and pages from the log otherwise. Each event's `seq` is its SSE `id`, so a reconnecting client resumes from `Last-Event-ID`.

```ts title="src/session.ts"
// Inside the Session class: streams one turn's events until its result
async events(
  session_id: string,
  request: Request,
  request_id: string,
): Promise<Response> {
  const runtime = await this.open(session_id)
  const after = Number(request.headers.get('Last-Event-ID') ?? 0)
  return this.hub.stream(
    after,
    // 1. Sends a keepalive comment after 15 seconds without events
    15_000,
    // 2. Reads from the tail, or from the log when the tail has moved
    // past the cursor
    async (cursor) =>
      this.hub.recent(cursor, request_id) ??
      runtime.page(scope, cursor, 128, request_id),
    // 3. Ends the stream after the turn's result event
    (event) => (event.event as { type?: string }).type === 'result',
  )
}
```

Every turn ends with a logged `result` event that has the turn's `status`. Without the last argument, the stream stays open for every later event.

### Route the Worker

The Worker routes each request to the session's object by name and calls its methods over RPC.

```ts title="src/worker.ts"
export { Session } from './session.js'

// Routes /sessions/:id/turns and /sessions/:id/events to the session's
// object
export default {
  async fetch(
    request: Request,
    env: { SESSION: DurableObjectNamespace<Session> },
  ): Promise<Response> {
    const [, , id, action] = new URL(request.url).pathname.split('/')
    if (!id) return new Response('Not found', { status: 404 })
    const session = env.SESSION.get(env.SESSION.idFromName(id))

    if (action === 'turns' && request.method === 'POST') {
      const { prompt, request_id } = (await request.json()) as {
        prompt: string
        request_id: string
      }
      return Response.json(await session.submit(id, prompt, request_id))
    }
    const turn = new URL(request.url).searchParams.get('turn')
    if (action === 'events' && turn)
      return session.events(id, request, turn)
    return new Response('Not found', { status: 404 })
  },
}
```

## Recipes

### Attach a Hand

A hand is a remote Code Mode executor that connects over WebSocket. `RemoteExecutor` carries the SDK's frames over the socket. Open the session with `remote: true` and pass the executor as the `remote` runtime option.

```ts
import { RemoteExecutor } from 'nanocode-wasm'

// 1. Accept the hand's socket and wait for its catalog frame
const executor = RemoteExecutor.accept(socket, {
  max_frame_bytes: 1 << 20,
  timeout_ms: 10_000,
})
socket.addEventListener('message', (event) =>
  executor.receive(event.data),
)
socket.addEventListener('close', () => executor.disconnected())
await executor.ready()

// 2. Run Code Mode on the hand
await runtime.open(
  { ...config, remote: true },
  {
    dispatch,
    remote: {
      cancel: (id) => executor.cancel(id),
      dispose: () => executor.dispose(),
      execute: (request, id) => executor.execute(request, id),
      failed: () => executor.failure !== undefined,
    },
  },
)
```

`accept` suits a socket whose messages the host forwards, as a hibernating Durable Object does. `RemoteExecutor.restore` rebuilds the executor after hibernation from the stored catalog frame. `refuse(reason)` closes the connection with code 1008 and the reason.

Several hands per session use `config.hands`, `attach`, and `detach`, with the connection ID in each remote request. [Remote Hands](/docs/guides/remote-hands) shows that routing.

### Release Sockets

A Codex subscription session holds a Responses WebSocket, which the `socket` runtime option opens. An idle object can close these sockets before it hibernates. `releaseConnections` closes them, and `connected` reports whether one is still open.

```ts
// Closes model sockets while the session has no turn to run
if (runtime.connected) runtime.releaseConnections()
```

### Dispose on Eviction

`dispose` shuts the SDK session down. The stored session remains in SQL, so the next instance continues it.

```ts
// Inside the Session class: the next request instantiates a new runtime
await (await this.runtime)?.dispose()
this.runtime = undefined
```

## More

- [Durability](/docs/guides/durability): The SqlHost contract and value limits.

- [Remote Hands](/docs/guides/remote-hands): Run Code Mode on remote machines.

- [Model Gateways](/docs/guides/gateways): Route Codex through AI Gateway and others.

- [nanocode on Cloudflare](/cloudflare/): Reset a Durable Object mid-turn in a live demo.

- [EventHub](/docs/api/wasm/EventHub): Reference for the event tail and SSE stream.

- [RemoteExecutor](/docs/api/wasm/RemoteExecutor): Reference for the remote Code Mode transport.

- [sessionUuid](/docs/api/wasm/sessionUuid): Make the UUIDv7 session IDs the SDK requires.
