# How Durable Objects Are Implemented for Onboarding and SAM Chat Agents in Open SEO

> Learn how Open SEO implements Durable Objects for stateful chat sessions with onboarding and SAM chat agents. Discover distinct namespaces, instance identifiers, and persistence strategies.

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

---

**Open SEO uses Cloudflare Durable Objects to maintain stateful, long-running chat sessions for both the free onboarding preview and the full SAM SEO assistant, with each agent type using distinct namespaces, instance identifiers, and persistence strategies backed by SQLite and Postgres.**

Open SEO leverages Cloudflare Durable Objects (DOs) to power its conversational AI features, ensuring that both the onboarding flow and the SAM in-app assistant maintain persistent state across WebSocket connections. This architecture allows each chat session to survive worker restarts while scaling horizontally across the Cloudflare edge network.

## Durable Object Architecture Overview

Open SEO defines two distinct Durable Object namespaces in [`src/env.d.ts`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts) to separate the onboarding experience from the production SAM assistant.

### Namespace Declarations

The environment interface declares separate bindings for each agent type:

```typescript
// src/env.d.ts
export interface Env {
  ONBOARDING_CHAT: DurableObjectNamespace; // Onboarding preview agent
  SAM_CHAT: DurableObjectNamespace;        // Full SAM assistant
}

```

### Agent Class Hierarchy

Each agent extends a different base class to match its complexity requirements:

- **OnboardingChatAgent** extends `AIChatAgent` and handles short, free-preview SEO conversations
- **SamChatAgent** extends `Think` and supports advanced context blocks and tool use

### Instance Identification

The runtime identifies specific DO instances using client-supplied names:

- **Onboarding** uses the **project ID** as the instance name (`useAgent({ name: projectId })`)
- **SAM** uses the **session ID** to isolate individual chat sessions (`useAgent({ name: sessionId })`)

## State Persistence and Storage

Each Durable Object receives a `DurableObjectState` object containing a SQLite-backed storage interface (`this.storage`) that persists data across requests.

### Onboarding Persistence Logic

In [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts), the agent persists chat history with automatic cleanup. The implementation caps stored messages at 60 entries and implements retry logic for transient storage errors around lines 70-80.

### SAM Session Storage

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) stores session metadata including the public origin URL. Lines 98-106 demonstrate how the constructor writes initial session data to `this.storage` during instantiation.

## Billing and Usage Gating

Both agents enforce billing checks before executing LLM calls, leveraging the `isHostedServerAuthMode` flag from [`src/server/lib/runtime-env.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/runtime-env.ts) to determine if credit validation is required.

### Onboarding Limits

The onboarding agent enforces a `FREE_ONBOARDING_QUESTION_LIMIT` and tracks OpenRouter LLM spend against per-organization credit pools using utilities from [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts).

### SAM Per-Turn Billing

SAM loads session data via [`src/server/features/sam/SamSessionRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamSessionRepository.ts) to resolve user emails for Autumn billing integration. Each turn accumulates costs in `turnCostUsd` before charging the organization's credit balance, ensuring no unauthorized model execution occurs.

## Context Blocks and Shared Memory

While the onboarding agent relies on simple message persistence, SAM implements sophisticated memory management.

### SAM Context Architecture

Extending the `Think` framework, `SamChatAgent` configures read-only "soul" blocks and writable "memory" and "research_log" blocks. According to lines 66-78 in [`src/server/features/sam/SamChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamChatAgent.ts), these blocks map to the Postgres `sam_project_memory` table via [`src/server/features/sam/SamProjectMemoryRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamProjectMemoryRepository.ts), enabling knowledge sharing across sessions within the same project.

## Request Lifecycle and Implementation Flow

The complete flow from client connection to response involves several coordinated steps:

1. **Client Initialization**: The client calls `useAgent()` with the appropriate binding (`ONBOARDING_CHAT` or `SAM_CHAT`) and instance name (project or session ID)
2. **Connection Handling**: The Worker's `onBeforeConnect` hook authenticates the request and forwards the WebSocket to the correct DO namespace
3. **Instantiation**: Cloudflare creates or fetches the DO instance where `this.name` matches the supplied identifier, invoking the constructor with `state` and `env`
4. **State Hydration**: The DO loads persisted data from its SQLite store (`this.storage`)
5. **Processing**: The agent executes billing checks, selects models, and builds tool contexts
6. **Persistence**: New messages and state changes are written back to SQLite before streaming responses to the client

## Client and Server Implementation Examples

### Onboarding Client Implementation

```typescript
import { useAgent } from "cloudflare:workers";

const onboarding = useAgent({
  name: projectId,               // DO instance identifier
  binding: "ONBOARDING_CHAT",   // Namespace from env.d.ts
});

await onboarding.sendMessage({ 
  role: "user", 
  content: "How does SEO work?" 
});

```

### SAM Client Implementation

```typescript
import { useAgent } from "cloudflare:workers";

const sam = useAgent({
  name: sessionId,            // Unique per chat session
  binding: "SAM_CHAT",       // SAM namespace
});

await sam.sendMessage({ 
  role: "user", 
  content: "Analyze my site" 
});

```

### Server-Side Registration

The Worker exports both classes in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts):

```typescript
export { OnboardingChatAgent } from "./server/features/onboarding/OnboardingChatAgent";
export { SamChatAgent } from "./server/features/sam/SamChatAgent";

```

## Key Source Files

- **[`src/env.d.ts`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts)**: Declares DO namespaces `ONBOARDING_CHAT` and `SAM_CHAT`
- **[`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts)**: Implements onboarding agent with 60-message persistence cap and billing gates
- **[`src/server/features/sam/SamChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamChatAgent.ts)**: Implements Think-based agent with context blocks and per-session billing
- **[`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts)**: Entry point exporting DO classes for Cloudflare runtime
- **[`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts)**: Provides `checkUsageCreditsDepleted` and `trackUsageCreditSpend` utilities
- **[`src/server/lib/runtime-env.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/runtime-env.ts)**: Exports `isHostedServerAuthMode` for billing conditional logic
- **[`src/server/features/projects/repositories/ProjectRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/projects/repositories/ProjectRepository.ts)**: Resolves project data from DO names
- **[`src/server/features/sam/SamSessionRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamSessionRepository.ts)**: Manages SAM session lookups
- **[`src/server/features/sam/SamProjectMemoryRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamProjectMemoryRepository.ts)**: Backs shared memory context blocks

## Summary

- Open SEO uses **two Durable Object namespaces** (`ONBOARDING_CHAT` and `SAM_CHAT`) to isolate onboarding and SAM functionality
- **OnboardingChatAgent** uses project IDs as instance names and caps SQLite storage at 60 messages
- **SamChatAgent** uses session IDs and extends the Think framework with Postgres-backed context blocks
- Both agents perform **pre-execution billing checks** using `isHostedServerAuthMode` to validate credits
- State persists automatically in **SQLite-backed Durable Object storage**, eliminating the need for external databases per session

## Frequently Asked Questions

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

**OnboardingChatAgent** extends `AIChatAgent` to provide a limited free preview using project IDs as instance identifiers, while **SamChatAgent** extends `Think` to offer full-featured SEO assistance with advanced context blocks and session-based isolation. The onboarding agent enforces strict message limits, whereas SAM supports persistent project-level memory.

### How does billing work for these Durable Object chat agents?

Both agents check `isHostedServerAuthMode` before executing LLM calls. Onboarding tracks usage against a `FREE_ONBOARDING_QUESTION_LIMIT` and per-org credit pools, while SAM accumulates `turnCostUsd` for each interaction and charges the organization's balance through Autumn integration.

### How is chat history persisted in Open SEO Durable Objects?

Each DO instance receives a `DurableObjectState` with a SQLite store (`this.storage`). Onboarding persists messages directly to SQLite with a 60-entry limit, while SAM combines SQLite session storage with Postgres-backed context blocks for shared project memory.

### What identifies a Durable Object instance for each agent type?

**Onboarding** instances are identified by **project ID**, allowing one chat session per project. **SAM** instances use **session ID**, creating isolated Durable Objects for each individual chat session. These identifiers are passed as the `name` parameter in `useAgent()` calls.