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

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 to separate the onboarding experience from the production SAM assistant.

Namespace Declarations

The environment interface declares separate bindings for each agent type:

// 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, 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 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 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.

SAM Per-Turn Billing

SAM loads session data via 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, these blocks map to the Postgres sam_project_memory table via 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

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

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:

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

Key Source Files

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →