How Durable Objects Handle SAM Chat and Onboarding Agents in OpenSEO

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:

// 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 ensure compile-time safety:

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

Runtime lookup uses the standard DO pattern:

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) 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) 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:

"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

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

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

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

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 →