# How OpenSEO Implements Stateful Chat with Cloudflare Durable Objects

> Discover how OpenSEO leverages Cloudflare Durable Objects and SQLite to implement stateful chat, persisting conversation context and transcripts across serverless invocations.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-09-09

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/SamChatAgent.ts)): Handles in-application chat sessions
- **`OnboardingChatAgent`** ([`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts)): Manages onboarding assistant conversations
- **`AuditScratchpad`** ([`src/server/features/audit/AuditScratchpad.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server.ts), the `onBeforeConnect` middleware validates the session against the `sam_sessions` database table and verifies project organization membership:

```typescript
// 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 `fetch` method override stores the public origin for deep-link tool compatibility:
  ```typescript
  // 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:

```typescript
// 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 via `buildSoulPrompt()`
- **`project_context`**: Exposes shared project memory enabling tools like `update_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`](https://github.com/every-app/open-seo/blob/main/src/env.d.ts) and bound in `wrangler.jsonc`:

```typescript
// 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.storage` persists transcripts, alarms, and auxiliary data across hibernation and restarts
- **Pre-Connection Auth**: The Worker validates JWTs and database permissions via `onBeforeConnect` before 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 `turnCostUsd` with subscription limit enforcement via `checkUsageCreditsDepleted`

## 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`](https://github.com/every-app/open-seo/blob/main/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.