The Role of Durable Objects in OpenSEO for Chat Agents: Architecture and Implementation
OpenSEO uses Cloudflare Durable Objects to provide stateful, isolated execution environments for its AI chat agents, handling persistent storage, authentication, billing enforcement, and GDPR-compliant data erasure while maintaining low-latency edge performance.
OpenSEO leverages Cloudflare Durable Objects (DOs) as the foundational infrastructure for every chat-agent interaction. According to the every-app/open-seo source code, these Durable Objects power two distinct agents—the free OnboardingChatAgent and the full-featured SamChatAgent—by managing WebSocket lifecycles, persisting conversation history, and enforcing credit limits before executing LLM calls.
Why OpenSEO Relies on Durable Objects for Chat Agents
The platform utilizes Durable Objects to overcome the stateless limitations of traditional Workers, providing five critical capabilities for production-grade chat applications.
Stateful, Persistent Storage
Unlike stateless Workers, Durable Objects maintain SQLite-backed storage that survives reloads and redeployments. In src/server/features/onboarding/OnboardingChatAgent.ts, the agent stores the full conversation history in this.messages, which maps directly to DurableObjectStorage (lines 62-66). This ensures that if a user refreshes their browser or the underlying Worker restarts, the chat context remains intact.
The SAM chat uses a hybrid approach: while the DO provides the isolated execution context, persistent storage is backed by Postgres through the withPgClient helper, as implemented in src/server/features/sam/SamChatAgent.ts (lines 74-79).
Isolation Per Chat Instance
Each conversation runs in its own Durable Object instance, guaranteeing strict scope isolation. The OnboardingChatAgent uses the project ID as its instance identifier, while SamChatAgent uses the session ID. This one-DO-per-chat pattern ensures that messages, billing counters, and tool usage remain confined to that specific conversation (OnboardingChatAgent.ts lines 70-78; SamChatAgent.ts lines 74-81). No chat can access another's data, and resource limits apply per-instance.
Built-in Authentication and Billing Enforcement
Durable Objects in OpenSEO do not handle raw authentication logic—instead, they trust that the Worker has already verified the caller. The authorizeChatAgent function in src/server.ts (lines 34-45 and 65-78) validates organization membership before routing requests via routeAgentAgentRequest. Once inside the DO, the instance name (project or session ID) is trusted for looking up records and applying credit metering.
Both agents implement trackUsageCreditSpend to record LLM usage against organization credits. The OnboardingChatAgent enforces limits before proceeding (lines 34-63), while SamChatAgent validates credits during initialization (lines 64-71).
Scalable, Low-Latency Edge Execution
DOs run colocated with the Worker in Cloudflare's edge environment, eliminating network round-trips when streaming LLM responses. The architecture allows the Durable Object to maintain an open WebSocket connection directly to the client while simultaneously querying Postgres or SQLite storage, resulting in sub-100ms response times for chat continuations.
Graceful Erasure for GDPR Compliance
Both agent classes implement destroyForErasure to satisfy strict data deletion requirements. When invoked, the method closes all active WebSockets, aborts pending LLM requests, and wipes the DO's SQLite storage using this.ctx.storage.deleteAll(). This pattern appears in OnboardingChatAgent.ts (lines 74-84) and SamChatAgent.ts (lines 7-16), ensuring complete user data removal during GDPR erasure workflows.
Architectural Flow of Chat Requests
Understanding how requests traverse the system clarifies why Durable Objects are essential to OpenSEO's reliability.
- Client → Worker: A WebSocket upgrade request hits
/agents/*endpoints defined insrc/server.ts. - Authorization Layer: The Worker executes
authorizeChatAgentviaonBeforeConnectoronBeforeRequesthooks, verifying the caller belongs to the correct organization and retrieving the project or session record. - DO Routing: The Worker forwards the validated request to the appropriate DO binding (
ONBOARDING_CHATorSAM_CHAT) usingrouteAgentRequest, passing the project or session ID as the instance name. - State Restoration: The Durable Object instance loads its persistent state from SQLite or Postgres, enforces credit limits, and initializes the chat context.
- Stream Execution: The DO runs LLM streaming logic, persists the conversation turn, updates billing records, and streams the response back through the WebSocket.
Implementation Patterns and Code Examples
These patterns from the OpenSEO codebase demonstrate production-ready Durable Object implementations.
Minimal Durable Object for Chat Agents
The following pattern extends AIChatAgent to create a stateful onboarding assistant:
import { AIChatAgent } from "@cloudflare/ai-chat";
export class OnboardingChatAgent extends AIChatAgent {
// Persist the last 50 messages in DO-SQLite
maxPersistedMessages = 50;
async onChatMessage(onFinish, options) {
// Load project-specific data using the trusted instance name
const project = await ProjectRepository.getProjectById(this.name);
if (!project) return staticAssistantResponse("Project not found");
// Run LLM streaming with context-aware system prompt
const result = streamText({
model: await getChatAgentModel(),
system: `You are an SEO assistant for ${project.domain}.`,
messages: await convertToModelMessages(this.messages),
onFinish,
});
return result.toUIMessageStreamResponse();
}
}
Key implementation details: the class uses this.name (the DO instance identifier) to lookup project data, relies on this.messages for automatic SQLite persistence, and trusts that the Worker already authorized the request.
Worker Authorization Pattern
Before any WebSocket reaches the Durable Object, the Worker performs strict validation:
async function authorizeOnboardingChat(request: Request, projectId: string) {
const ctx = await resolveUserContextFromHeaders(request.headers);
const project = await ProjectRepository.getProjectForOrganization(
projectId,
ctx.organizationId,
);
if (!project) return new Response("Forbidden", { status: 403 });
// Ensure billing record exists before DO checks credits
if (await isHostedServerAuthMode()) {
await getOrCreateOrganizationCustomer(ctx);
}
return undefined; // allow the DO to be reached
}
This authorization runs in src/server.ts before routeAgentRequest forwards the connection, ensuring the Durable Object can safely trust its name parameter without re-validating permissions.
GDPR Erasure Implementation
Both agents implement immediate data destruction for compliance:
async destroyForErasure() {
// Terminate all active client connections
for (const socket of this.ctx.getWebSockets()) {
socket.close(1000, "Account erased");
}
// Cancel pending LLM requests
this.abortAllRequests("GDPR erasure");
// Wipe SQLite storage completely
await this.ctx.storage.deleteAll();
}
This method is invoked by the GDPR erasure workflow to guarantee no chat transcripts remain in edge storage.
Summary
- Durable Objects provide stateful isolation: Each chat runs in its own DO instance (project ID or session ID), ensuring data isolation and persistent storage across Worker restarts.
- Security is layered: The Worker handles authentication via
authorizeChatAgentbefore the DO receives requests, allowing the Durable Object to trust its instance name for billing and data lookups. - Storage is flexible: Onboarding uses SQLite within the DO (
this.messages), while SAM uses Postgres viawithPgClient, both benefiting from DO lifecycle management. - Compliance is built-in: The
destroyForErasuremethod ensures complete data deletion for GDPR requests, closing WebSockets and wiping storage atomically. - Performance is optimized: Colocation with Workers enables direct WebSocket streaming and fast access to edge databases, minimizing latency for chat responses.
Frequently Asked Questions
What is the difference between OnboardingChatAgent and SamChatAgent in OpenSEO?
OnboardingChatAgent extends AIChatAgent and handles the free preview chat for new users, identified by project ID and storing data in SQLite. SamChatAgent extends Think and provides the full in-app SEO assistant, identified by session ID and persisting data to Postgres via withPgClient. Both enforce credit limits but target different user journeys within the application.
How does OpenSEO handle GDPR data erasure for chat agents?
Both Durable Object classes implement destroyForErasure, which iterates through active WebSockets via this.ctx.getWebSockets() to close connections, calls abortAllRequests to stop pending LLM calls, and executes this.ctx.storage.deleteAll() to wipe SQLite data. This method is triggered by the GDPR erasure workflow to ensure complete user data removal from edge storage.
Why does OpenSEO use Durable Objects instead of stateless Workers for chat?
Stateless Workers cannot maintain WebSocket connections or persist conversation history across requests. Durable Objects provide stateful execution, SQLite storage, and instance-scoped isolation required for multi-turn conversations. They also enable per-chat billing enforcement and GDPR erasure capabilities that would be impossible with purely ephemeral Workers.
How is authentication handled before requests reach the Durable Object?
The Worker in src/server.ts runs authorizeChatAgent (via onBeforeConnect for WebSockets or onBeforeRequest for HTTP) to validate JWT tokens, verify organization membership, and load project/session records. Only after this validation does the Worker call routeAgentRequest to forward the connection to the Durable Object, which then trusts the instance name (this.name) for internal lookups without re-checking permissions.
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 →