How OpenSEO Implements Stateful Chat with Cloudflare Durable Objects
OpenSEO persists conversation state across serverless invocations by assigning each chat session to a dedicated Cloudflare Durable Object instance, using SQLite-backed storage to maintain transcripts, context, and billing data across hibernation cycles.
The every-app/open-seo repository demonstrates production-grade patterns for building resilient chat applications on the edge. By leveraging Cloudflare Durable Objects, OpenSEO moves beyond ephemeral stateless requests to provide reliable, per-session persistence that survives worker restarts and network interruptions.
Per-Session Durable Object Architecture
OpenSEO routes every chat session to its own Durable Object (DO) instance, ensuring complete isolation between conversations. The architecture centers on three primary DO implementations:
SamChatAgent(src/server/features/sam/SamChatAgent.ts): Handles in-application chat sessionsOnboardingChatAgent(src/server/features/onboarding/OnboardingChatAgent.ts): Manages onboarding assistant conversationsAuditScratchpad(src/server/features/audit/AuditScratchpad.ts): Provides per-audit scratchpad functionality beyond standard chat
Each class extends Cloudflare's DurableObject base class and the Think framework, inheriting streaming capabilities, history compaction, and tool execution logic. The client initiates a session by calling useAgent({ name: sessionId }), where the sessionId becomes the DO instance name, ensuring deterministic routing for subsequent requests.
Authorization Before Connection
Security is enforced at the Worker level before any WebSocket or HTTP traffic reaches the Durable Object. In src/server.ts, the onBeforeConnect middleware validates the session against the sam_sessions database table and verifies project organization membership:
// src/server.ts
export default {
async fetch(request, env) {
// Authorise incoming socket before routing to a DO.
const sessionId = await onBeforeConnect(request, env);
// Route to appropriate Durable Object.
const doNamespace = env.SAM_CHAT;
const stub = doNamespace.get(doNamespace.idFromString(sessionId));
return stub.fetch(request);
},
};
This pattern guarantees that only authenticated users with valid project permissions can instantiate or reconnect to a specific chat DO.
Persistent State with SQLite Storage
Unlike ephemeral Worker memory, Durable Objects utilize this.ctx.storage—a SQLite-backed transactional storage API—to persist data across hibernation. The storage survives indefinitely, allowing a conversation to resume exactly where it left off even after the DO wakes from hibernation without a preceding fetch request.
Key persistence patterns in SamChatAgent include:
-
Transcript storage: Conversation history maintained via
this.ctx.storage.put -
Origin tracking: The
fetchmethod override stores the public origin for deep-link tool compatibility:// src/server/features/sam/SamChatAgent.ts async fetch(request: Request): Promise<Response> { await this.ctx.storage.put(PUBLIC_ORIGIN_KEY, getPublicOrigin(request)); return super.fetch(request); } -
Cost accumulation: Per-turn OpenRouter costs tracked in
this.turnCostUsd
Context Loading and Project State
Each Durable Object loads session context once per lifetime using loadSamContext(), caching the result in this.samContext to avoid redundant database queries:
// src/server/features/sam/SamChatAgent.ts
private async loadSamContext(): Promise<SamContext | null> {
if (this.samContext) return this.samContext;
const row = await SamSessionRepository.getSessionById(this.name);
if (!row) return null;
const project = await ProjectRepository.getProjectById(row.projectId);
if (!project) return null;
const [creator] = await db.select({ email: user.email })
.from(user)
.where(eq(user.id, row.userId))
.limit(1);
if (!creator) return null;
this.samContext = { row, project, userEmail: creator.email };
return this.samContext;
}
The configureSession() method injects project-wide shared memory into the Think framework as read-only context blocks:
soul: Provides personality and behavioral guidelines viabuildSoulPrompt()project_context: Exposes shared project memory enabling tools likeupdate_project_context
Billing and Usage Metering
OpenSEO enforces subscription limits at the Durable Object level. The turnCostUsd accumulator tracks OpenRouter API costs per conversation turn, while trackUsageCreditSpend and checkUsageCreditsDepleted methods query the cached samContext to validate available credits before processing expensive operations.
Deployment and Namespace Configuration
Durable Object namespaces are declared in src/env.d.ts and bound in wrangler.jsonc:
// src/env.d.ts
export interface Env {
ONBOARDING_CHAT: DurableObjectNamespace;
SAM_CHAT: DurableObjectNamespace;
AUDIT_SCRATCHPAD: DurableObjectNamespace;
}
The wrangler.jsonc configuration maps these namespaces to their implementation classes, enabling the Worker to route traffic to the correct DO based on the request path.
Summary
- Isolated Sessions: Each chat session receives a dedicated Durable Object instance identified by
sessionId, preventing cross-talk between users - Durable Storage: SQLite-backed
this.ctx.storagepersists transcripts, alarms, and auxiliary data across hibernation and restarts - Pre-Connection Auth: The Worker validates JWTs and database permissions via
onBeforeConnectbefore handing off to the DO - Framework Integration: All chat agents extend the Think framework for streaming responses, tool execution, and history management
- Cost Tracking: Per-turn billing accumulates in
turnCostUsdwith subscription limit enforcement viacheckUsageCreditsDepleted
Frequently Asked Questions
How does OpenSEO ensure chat persistence across server restarts?
OpenSEO stores all conversation state in this.ctx.storage within the Durable Object rather than in Worker memory. Because Durable Objects run on Cloudflare's edge and persist to SQLite-backed storage, the conversation transcript survives indefinitely—even if the underlying Worker process restarts or the DO hibernates due to inactivity.
What authorizes a user to access a specific chat Durable Object?
Authorization occurs in src/server.ts through the onBeforeConnect function. The Worker extracts the session ID from the request, queries the sam_sessions table, and verifies the user's project membership before creating or retrieving the Durable Object stub. Unauthorized requests never reach the DO's fetch handler.
How does the Think framework integrate with Durable Objects?
All chat agents (SamChatAgent, OnboardingChatAgent) extend the Think class rather than the base DurableObject. Think provides the conversation loop, streaming HTTP responses, tool execution infrastructure, and history compaction. The DO's role is to manage persistence via this.ctx.storage and inject project-specific context through configureSession().
How is usage billing tracked per chat session?
Each Durable Object maintains a turnCostUsd counter that accumulates OpenRouter API costs during conversation turns. The object periodically calls trackUsageCreditSpend against the cached project context, and checkUsageCreditsDepleted validates remaining credits before initiating expensive operations, enforcing subscription limits at the edge.
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 →