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 viacheckUsageCreditsDepleted() - Per-turn cost capture:
turnCostUsdis 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_LIMITrestricts unpaid usage - Credit depletion guards:
checkUsageCreditsDepleted()blocks exhausted accounts - Cost aggregation: The
onFinishcallback ofstreamTexttallies 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:
- Client initiation: React code calls
useAgent({ name: sessionId })from the Agents SDK - Worker validation:
onBeforeConnectauthenticates and authorizes the user - DO routing: The Worker retrieves or creates the DO stub via
env.SAM_CHAT.get(id) - Context hydration:
SamChatAgentloads session/project data into memory - Chat execution: The
Thinkbase class streams LLM responses, invoking tools as needed - Billing hooks:
beforeTurn,onStepFinish, andonChatResponserecord usage - 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) andOnboardingChatAgent(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.jsoncdeclarations paired withsrc/env.d.tsprovide 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →