# How Durable Objects Handle SAM Chat and Onboarding Agents in OpenSEO

> Discover how OpenSEO leverages Cloudflare Durable Objects for stateful AI chat agents like SAM Chat and Onboarding Chat, ensuring isolation, persistence, and integrated billing.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-08-14

---

**OpenSEO uses Cloudflare Durable Objects to run two stateful AI chat agents—SAM Chat and Onboarding Chat—with per-session and per-project isolation, SQLite persistence, and integrated billing.**

The every-app/open-seo codebase implements a production AI platform entirely on Cloudflare's edge infrastructure. Both chat agents run as **Durable Objects (DOs)**, leveraging Cloudflare's SQLite-backed storage to maintain conversation state, enforce usage limits, and survive worker restarts without external databases.

## Durable Object Architecture Overview

OpenSEO declares two agent types in `wrangler.jsonc`, each with distinct instance scoping:

| Agent | DO Class | Instance Key | Purpose |
|-------|----------|--------------|---------|
| **SAM Chat** | `SamChatAgent` | Session ID | In-app SEO assistant for active projects |
| **Onboarding Chat** | `OnboardingChatAgent` | Project ID | Free preview assistant for new users |

This design ensures **isolation guarantees**: SAM conversations are scoped to individual chat sessions, while onboarding flows are tied to projects—preventing cross-contamination of state between users.

### Binding Configuration

The DO bindings in `wrangler.jsonc` expose both agent types to the Worker runtime:

```jsonc
// wrangler.jsonc
"durable_objects": {
  "bindings": [
    { "name": "ONBOARDING_CHAT", "class_name": "OnboardingChatAgent" },
    { "name": "SAM_CHAT",       "class_name": "SamChatAgent" }
  ]
}

```

These become typed fields on the `Env` object. TypeScript declarations in [`src/env.d.ts`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts) ensure compile-time safety:

```ts
// src/env.d.ts
declare namespace Cloudflare {
  interface Env {
    ONBOARDING_CHAT: DurableObjectNamespace;
    SAM_CHAT:        DurableObjectNamespace;
  }
}

```

Runtime lookup uses the standard DO pattern:

```ts
const stub = env.SAM_CHAT.get(env.SAM_CHAT.idFromName(sessionId));

```

## SAM Chat: The Project-Scoped SEO Assistant

`SamChatAgent` (located in [`src/server/features/sam/SamChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamChatAgent.ts)) extends the `Think` base class to provide context-aware SEO assistance.

### Instance Lifecycle and Context Loading

Each `SamChatAgent` instance is named by its **chat session ID** (`this.name`). On first connection, `loadSamContext()` fetches the associated `sam_sessions` record, project details, and creator email—caching this data for the DO's lifetime to avoid repeated database hits.

Connection authorization happens in `onBeforeConnect`, which validates the caller before the DO accepts the WebSocket. As noted in the source (lines 70-76), the DO trusts that validated callers may act on `this.name`, allowing safe derivation of project and user context from the instance identifier.

### Model Configuration and Tool Integration

The `getModel()` method constructs an OpenRouter-powered LLM instance. Before each turn, `beforeTurn()` injects project-scoped tools via `buildSamMcpTools()`—enabling the agent to interact with the user's actual SEO data.

### Billing and Cost Tracking

SAM Chat implements granular usage accounting:

- **Pre-flight check**: `beforeTurn()` verifies the organization's credit balance via `checkUsageCreditsDepleted()`
- **Per-turn cost capture**: `turnCostUsd` is recorded during streaming
- **Post-response persistence**: `onChatResponse()` commits usage records to the billing system

### SQL Storage and Data Erasure

All messages persist to the DO's SQLite storage (`this.ctx.storage`). The `fetch` handler additionally records the public origin for deep-link tool functionality.

For GDPR compliance, `destroyForErasure()` performs complete cleanup: websocket termination, pending chat cancellation, alarm clearing, and storage wiping.

## Onboarding Chat: The Free Preview Agent

`OnboardingChatAgent` (in [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts)) extends `AIChatAgent` to provide a limited, zero-friction entry point for new users.

### Instance Scoping and Message Limits

Unlike SAM Chat, onboarding uses the **project ID** as its instance key—one DO per project. A hard cap of `maxPersistedMessages = 60` prevents storage bloat for transient preview interactions.

### Quota Enforcement and Billing

Hosted deployments enforce strict limits:

- **Free tier cap**: `FREE_ONBOARDING_QUESTION_LIMIT` restricts unpaid usage
- **Credit depletion guards**: `checkUsageCreditsDepleted()` blocks exhausted accounts
- **Cost aggregation**: The `onFinish` callback of `streamText` tallies total spend

### System Prompt and Tool Construction

`buildSystemPrompt()` assembles a static prompt including the OpenSEO fact sheet, while `buildOnboardingTools()` supplies read-only SEO capabilities: website analysis, metrics retrieval, and keyword research—enough to demonstrate value without full project access.

### Resilient Persistence

`persistMessages()` implements retry logic for SQLite's transient error code 10001, attempting writes up to three times before failing.

Erasure follows the same pattern as SAM Chat: `destroyForErasure()` aborts in-flight requests, resets turn state, clears alarms, and purges storage.

## SQLite Schema Migrations

Local development and production deployments use Wrangler-managed migrations. The `wrangler.jsonc` migration chain establishes storage schemas:

```jsonc
"migrations": [
  { "tag": "v1", "new_sqlite_classes": ["OnboardingChatAgent"] },
  { "tag": "v2", "new_sqlite_classes": ["SamChatAgent"] },
  { "tag": "v3", "new_sqlite_classes": ["AuditScratchpad"] }
]

```

These run automatically during `wrangler dev`, ensuring each DO class has appropriate tables before accepting connections.

## End-to-End Request Flow

Understanding how Durable Objects handle SAM Chat and Onboarding Chat requires tracing the complete request lifecycle:

1. **Client initiation**: React code calls `useAgent({ name: sessionId })` from the Agents SDK
2. **Worker validation**: `onBeforeConnect` authenticates and authorizes the user
3. **DO routing**: The Worker retrieves or creates the DO stub via `env.SAM_CHAT.get(id)`
4. **Context hydration**: `SamChatAgent` loads session/project data into memory
5. **Chat execution**: The `Think` base class streams LLM responses, invoking tools as needed
6. **Billing hooks**: `beforeTurn`, `onStepFinish`, and `onChatResponse` record usage
7. **State persistence**: Messages commit to SQLite for durability across reconnections

The onboarding flow mirrors this sequence with `OnboardingChatAgent` and project-level identifiers.

## Implementation Examples

### Frontend: Connecting to SAM Chat

```ts
import { useAgent } from "agents/react";

function SamChat({ sessionId }: { sessionId: string }) {
  const { send, messages, isConnected } = useAgent({
    name: sessionId,            // DO instance = session ID
    binding: "SAM_CHAT",       // Must match wrangler.jsonc
  });

  // Render UI, call send(message) for user input
}

```

### Server: Programmatic Message Injection

```ts
import { env } from "cloudflare:workers";

async function notifySam(sessionId: string, text: string) {
  const stub = env.SAM_CHAT.get(env.SAM_CHAT.idFromName(sessionId));
  await stub.fetch(
    new Request("/message", {
      method: "POST",
      body: JSON.stringify({ text }),
    })
  );
}

```

### Compliance: Complete Data Erasure

```ts
import { env } from "cloudflare:workers";

export async function handleErasure(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const doName = url.searchParams.get("projectId")!;
  
  const stub = env.ONBOARDING_CHAT.get(
    env.ONBOARDING_CHAT.idFromName(doName)
  );
  
  await stub.fetch(new Request("/erase", { method: "POST" }));
  return new Response("Erased");
}

```

## Summary

- **Two DO classes**: `SamChatAgent` (session-scoped) and `OnboardingChatAgent` (project-scoped) power OpenSEO's conversational AI
- **SQLite persistence**: Messages and state survive worker restarts without external databases
- **Integrated billing**: Both agents track per-turn costs and enforce credit limits through hook-based instrumentation
- **Erasure-ready**: `destroyForErasure()` methods enable GDPR-compliant data deletion
- **Type-safe bindings**: `wrangler.jsonc` declarations paired with [`src/env.d.ts`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts) provide compile-time guarantees

## Frequently Asked Questions

### What is the difference between SAM Chat and Onboarding Chat Durable Objects?

SAM Chat uses session ID as its instance key, providing isolated conversations for active project work with full tool access and detailed billing. Onboarding Chat uses project ID, offers a capped 60-message preview experience with read-only tools, and enforces free-tier limits before requiring payment.

### How does OpenSEO prevent billing abuse in chat agents?

Both DO implementations call `checkUsageCreditsDepleted()` before processing turns. Onboarding Chat additionally enforces `FREE_ONBOARDING_QUESTION_LIMIT` for hosted environments. Costs are captured per-turn via `beforeTurn()` hooks and committed through `onChatResponse()` or `onFinish` callbacks.

### Why use Durable Objects instead of external databases for chat state?

Durable Objects colocate compute and SQLite storage on Cloudflare's edge, eliminating network latency between application logic and persistence. This architecture provides automatic request routing to the correct instance, seamless WebSocket handling, and built-in fault tolerance without operational overhead of managing separate database infrastructure.

### How does data erasure work for compliance requirements?

Each agent implements `destroyForErasure()`: SAM Chat terminates websockets, cancels pending generations, clears alarms, and wipes `this.ctx.storage`. Onboarding Chat adds request abortion and turn state reset. These methods are exposed via dedicated `/erase` endpoints for programmatic invocation during account deletion workflows.