Durable Objects Architecture for Chat Agents in OpenSEO: A Technical Deep Dive
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 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.
// 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 to provide type-safe access to the Durable Object namespaces. This ensures the Worker can reference DO stubs with full IntelliSense support.
// 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, 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.
/** 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 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.
/** 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.
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—
OnboardingChatAgentandSamChatAgent—inwrangler.jsoncwith 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
useAgentfromagents/react, passing the binding name and instance ID to establish WebSocket connections to the correct DO. - Both agents leverage
this.ctx.storagefor 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.
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 →