# Durable Objects Architecture for Chat Agents in OpenSEO: A Technical Deep Dive

> Explore the Durable Objects architecture for OpenSEO chat agents. Discover how DOs ensure persistent state and isolation for reliable, scalable conversations.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-09-04

---

**OpenSEO leverages Cloudflare Durable Objects (DOs) to provide stateful, isolated runtime environments for its chat agents, ensuring conversation history and billing state persist across server restarts while maintaining strict per-project and per-session isolation.**

The open-source SEO platform [every-app/open-seo](https://github.com/every-app/open-seo) implements two distinct chat agents using Cloudflare's Durable Objects architecture. This design pattern gives each conversation its own dedicated SQLite-backed runtime that survives hibernation, scaling events, and code deployments without losing context.

## Configuring Durable Object Bindings in wrangler.jsonc

Durable Object registrations begin in **`wrangler.jsonc`**, where each chat agent class is bound to a named export available in the Worker environment. The configuration distinguishes between the onboarding flow and the full-featured assistant.

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

```

These bindings map the `ONBOARDING_CHAT` and `SAM_CHAT` namespace identifiers to their respective TypeScript implementations. When the Worker receives a request, it uses these bindings to locate or create the specific DO instance responsible for that conversation.

## TypeScript Environment Types for DO Namespaces

The project augments Cloudflare's ambient type declarations in **[`src/env.d.ts`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts)** to provide type-safe access to the Durable Object namespaces. This ensures the Worker can reference DO stubs with full IntelliSense support.

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

```

This declaration allows the Worker to instantiate stubs via `env.ONBOARDING_CHAT.get(id)` or `env.SAM_CHAT.get(id)`, returning a `DurableObjectStub` that routes WebSocket and HTTP calls to the correct isolated runtime.

## OnboardingChatAgent: Per-Project Stateful Sessions

The **`OnboardingChatAgent`** class, defined in [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts), manages short, free-preview conversations tied to a specific project. Each DO instance handles exactly one project throughout its lifetime.

### Instance Lifetime and Storage

The DO uses the project UUID as its instance name (`this.name`), guaranteeing that all users accessing the same project ID connect to the same underlying Durable Object. Chat transcripts persist in the DO's built-in SQLite storage via `this.ctx.storage`.

```typescript
/** Durable Object backing the onboarding strategy chat. */
export class OnboardingChatAgent extends AIChatAgent {
  maxPersistedMessages = 60;
  
  async persistMessages(...args) {
    // Retry logic for transient SQLite errors
  }
}

```

The class caps storage at **`maxPersistedMessages = 60`** to prevent unbounded growth, implementing retry logic for transient SQLite write failures. The Worker authorizes connections in `onBeforeConnect` to ensure callers can only access DOs matching their authenticated project ID.

### GDPR Erasure and Retry Logic

For compliance, the agent implements **`destroyForErasure()`**, which closes active WebSockets, aborts pending operations, and wipes the SQLite storage associated with the project ID.

## SamChatAgent: Per-Session AI Agents with Think Framework

The **`SamChatAgent`** class in [`src/server/features/sam/SamChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamChatAgent.ts) powers the full-featured SEO assistant. Unlike the onboarding agent, Sam creates one DO instance per chat session, using the session UUID as the instance key.

### Think Framework Integration

`SamChatAgent` extends **Think**, Cloudflare's agentic framework that supplies the core reasoning loop, streaming infrastructure, and tool execution engine. Sam layers SEO-specific capabilities on top of this foundation.

```typescript
/** Durable Object backing the SAM in-app agent. */
export class SamChatAgent extends Think {
  private samContext: SamContext | null = null;
  
  async beforeTurn(_ctx) {
    // Billing gate implementation
  }
  
  async onChatResponse(result) {
    // Credit charging logic
  }
}

```

The integration provides structured context blocks including a **`project_context`** block rendering shared project memory (competitors, target keywords) and a **`soul`** block generating dynamic system prompts from live project data.

### Billing and Metering Hooks

SamChatAgent implements **`beforeTurn()`** to gate usage behind credit checks and **`onChatResponse()`** to accumulate OpenRouter costs in `turnCostUsd`, reporting charges to the credit-tracking service immediately after each AI response completes.

### Context Blocks and Memory

The DO maintains session history in SQLite while accessing project-wide shared memory stored externally, creating a hybrid persistence model where conversation state is isolated but business context remains shared across sessions.

## Connecting Frontend Clients to Durable Objects

The React frontend uses the **`useAgent`** hook from `agents/react` to create typed stubs that communicate with the Durable Objects via WebSockets.

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

// Onboarding chat – one instance per project
const onboarding = useAgent({ 
  name: projectId, 
  binding: "ONBOARDING_CHAT" 
});
await onboarding.sendMessage({ 
  role: "user", 
  content: "How does SEO work?" 
});

// SAM chat – one instance per session
const sam = useAgent({ 
  name: sessionId, 
  binding: "SAM_CHAT" 
});
await sam.sendMessage({ 
  role: "user", 
  content: "Give me a keyword plan." 
});

```

The `binding` parameter must match the keys defined in `wrangler.jsonc`, while the `name` parameter determines the DO instance key—project ID for onboarding, session ID for SAM. Cloudflare's runtime routes these calls to the specific SQLite-backed process hosting that conversation state.

## Architectural Benefits of Durable Objects for Chat

**Stateful isolation** ensures that messages, billing accumulators, and temporary computation data never leak between projects or concurrent sessions. Each chat operates in its own V8 isolate with dedicated storage.

**Automatic persistence** via `this.ctx.storage` eliminates the need for external databases to maintain conversation history. The chat transcript survives Worker restarts, hibernation, and redeployment events without application-level checkpointing code.

**Scalable routing** allows Cloudflare to distribute DO instances across its edge network while maintaining strong consistency guarantees. The system scales horizontally by creating new DO instances on demand, with each instance handling its own local state.

**Fine-grained authorization** occurs at the Worker layer before the request reaches the DO. The `onBeforeConnect` hook validates JWTs and enforces that callers can only instantiate or connect to DOs matching their permission scope.

## Summary

- OpenSEO configures two Durable Object classes—`OnboardingChatAgent` and `SamChatAgent`—in `wrangler.jsonc` with distinct namespace bindings.
- **OnboardingChatAgent** uses project IDs as instance keys, persists up to 60 messages per project, and implements GDPR-compliant erasure via `destroyForErasure()`.
- **SamChatAgent** extends the Think framework, uses session IDs as instance keys, and integrates billing hooks (`beforeTurn`, `onChatResponse`) for usage-based pricing.
- Frontend clients connect via `useAgent` from `agents/react`, passing the binding name and instance ID to establish WebSocket connections to the correct DO.
- Both agents leverage `this.ctx.storage` for automatic SQLite persistence, ensuring chat state survives serverless Worker hibernation and restarts.

## Frequently Asked Questions

### How does OpenSEO isolate chat sessions using Durable Objects?

Each chat session runs in its own Durable Object instance, identified by either a project ID (OnboardingChatAgent) or session ID (SamChatAgent). Cloudflare's runtime guarantees that only one instance of each named DO exists globally, and each instance operates in an isolated V8 runtime with its own SQLite storage, preventing data leakage between conversations.

### What is the difference between OnboardingChatAgent and SamChatAgent?

**OnboardingChatAgent** handles short, free-preview chats scoped to a project ID with a 60-message limit, while **SamChatAgent** provides full-featured SEO assistance using the Think framework, scoped to individual session IDs with integrated billing, tool execution, and access to project-shared memory. The onboarding agent extends `AIChatAgent`, whereas Sam extends `Think` for agentic capabilities.

### How does OpenSEO handle GDPR data erasure in chat agents?

Both DO classes implement the **`destroyForErasure()`** method, which closes all active WebSocket connections, aborts pending AI requests, and calls storage deletion APIs to wipe the SQLite database associated with that specific DO instance. This ensures complete removal of conversation history and metadata when a user requests data deletion.

### What storage mechanism persists chat history across worker restarts?

The architecture uses **`this.ctx.storage`**, the built-in SQLite-backed storage API provided by Cloudflare Durable Objects. This storage is strongly consistent and durable, automatically persisting the `messages` array and billing state to disk without requiring external database calls or manual serialization logic in the application code.