# How the OpenSEO Onboarding Chat System Works: Architecture and Implementation

> Discover how the OpenSEO onboarding chat system works. Learn about its durable-object AI architecture, client-side gating, and server-side credit metering for 7 free SEO strategy questions.

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

---

**The OpenSEO onboarding chat system is a durable-object-based AI agent that provides new users with 7 free SEO strategy questions before requiring a subscription, enforcing limits through both client-side gating and server-side credit metering.**

OpenSEO's onboarding chat functions as a short-lived, credit-metered assistant that demonstrates the platform's SEO capabilities to prospective users. The system combines **Durable Objects** for stateful persistence, **SQLite** for conversation storage, and **tool-augmented LLM calls** to deliver contextual strategy advice. This article breaks down the complete architecture based on the every-app/open-seo source code.

---

## Client-Side Gating: The Free Question Limit

The onboarding experience begins with a hardcoded constant that synchronizes client and server behavior.

```typescript
// src/shared/onboardingChat.ts
export const FREE_ONBOARDING_QUESTION_LIMIT = 7;

```

This `FREE_ONBOARDING_QUESTION_LIMIT` serves as the single source of truth across both environments. The client UI consumes this constant to:

- Display "X questions left" indicators
- Disable the message composer when the limit is reached
- Trigger subscription prompts at the appropriate moment

Keeping this value in a shared module ensures the client preview never diverges from server enforcement.

---

## Server-Side Architecture: OnboardingChatAgent

The core of the OpenSEO onboarding chat is the `OnboardingChatAgent` class, a **Durable Object** that extends `AIChatAgent` and lives in [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts).

### Message Persistence and State

The agent maintains conversation history in SQLite, with automatic retry logic for transient database errors. The `persistMessages` method guarantees durability even under load spikes.

### Dual Enforcement: Count and Credits

When processing a `onChatMessage` request, the agent performs two sequential checks:

1. **Question count** – Filters stored messages by `role === 'user'` and compares against `FREE_ONBOARDING_QUESTION_LIMIT`. If exceeded and the organization lacks managed access, returns a static subscription response (lines 31-38).

2. **Credit balance** – In hosted mode, calls `checkUsageCreditsDepleted` to verify onboarding credits remain. If depleted, returns the same subscription prompt (lines 42-48).

```typescript
// Core enforcement logic from OnboardingChatAgent.ts
async onChatMessage(onFinish, options) {
  const project = await ProjectRepository.getProjectById(this.name);
  const questionCount = this.messages.filter(m => m.role === 'user').length;

  // Free-question cap enforcement
  if (questionCount > FREE_ONBOARDING_QUESTION_LIMIT && 
      !(await customerHasManagedAccess(project.organizationId))) {
    return staticAssistantResponse(
      'You\'ve used all your free strategy questions. Subscribe to continue.'
    );
  }

  // Credit enforcement in hosted environments
  if (await checkUsageCreditsDepleted(project.organizationId, 'onboarding')) {
    return staticAssistantResponse('Subscribe to continue.');
  }
  // ... LLM orchestration
}

```

---

## LLM Orchestration and the "Sam" Persona

The OpenSEO onboarding chat presents itself as **"Sam"**, an SEO strategy assistant. The `buildSystemPrompt` method (lines 26-60) constructs detailed instructions governing:

- **Tone and style** – Professional but approachable SEO expert voice
- **Content boundaries** – Allowed topics and response patterns
- **Tool availability** – Which capabilities the model may invoke

### Available Tools for the Onboarding Assistant

The `buildOnboardingTools` factory exposes SEO-specific capabilities:

| Tool | Purpose |
|------|---------|
| `read_website` | Fetches and analyzes page content |
| `get_seo_metrics` | Retrieves performance indicators |
| `research_keywords` | Identifies keyword opportunities |

These tools allow the onboarding chat to demonstrate real OpenSEO functionality rather than generic advice.

---

## Billing Integration and Cost Tracking

The OpenSEO onboarding chat system meters actual LLM spend against organization credits:

- **Token budgeting** – Each request caps output at 4000 tokens
- **Per-step cost tracking** – `streamText` runs with cost instrumentation
- **Credit consumption** – `trackUsageCreditSpend` records expenditure against the organization's onboarding credit pool (lines 70-84)

This ensures free previews remain economically bounded while providing genuine utility.

---

## Key Files and Responsibilities

| File Path | Role in Onboarding Chat |
|-----------|------------------------|
| [`src/shared/onboardingChat.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/onboardingChat.ts) | Shared constants (`FREE_ONBOARDING_QUESTION_LIMIT`) |
| [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts) | Durable Object agent, enforcement, LLM orchestration |
| [`src/server/features/onboarding/onboardingChatTools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/onboardingChatTools.ts) | Tool factories (`read_website`, `get_seo_metrics`, etc.) |
| [`src/server/lib/openrouter.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/openrouter.ts) | Model selection and cost reporting |
| [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) | Credit checking and tracking utilities |
| [`src/server/lib/chatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/chatAgent.ts) | Base agent utilities and cost calculations |

---

## Summary

- **The OpenSEO onboarding chat** provides 7 free strategy questions via a synchronized client-server limit (`FREE_ONBOARDING_QUESTION_LIMIT`)
- **The `OnboardingChatAgent` Durable Object** enforces both message-count and credit-based restrictions before serving LLM responses
- **The "Sam" persona** operates through a constructed system prompt with access to real SEO tools (`read_website`, `get_seo_metrics`, `research_keywords`)
- **Cost tracking** via `trackUsageCreditSpend` binds free usage to actual LLM expenditure, preventing abuse
- **SQLite persistence** with retry logic ensures conversation state survives restarts and errors

---

## Frequently Asked Questions

### What happens when a user exceeds the free question limit in OpenSEO?

The `OnboardingChatAgent` counts user messages in its SQLite store. Once the count exceeds `FREE_ONBOARDING_QUESTION_LIMIT` (7) and the organization lacks managed access, the agent returns a static response prompting subscription. The client UI simultaneously disables the composer based on the same constant.

### How does OpenSEO prevent unlimited free usage of the onboarding chat?

The system implements **dual enforcement**: message counting for immediate gating, plus credit balance checking via `checkUsageCreditsDepleted` for hosted deployments. Even if a client bypasses the UI limit, the server rejects requests when `trackUsageCreditSpend` indicates exhausted credits.

### What tools can the onboarding chat assistant use?

According to `buildSystemPrompt` and `buildOnboardingTools`, the assistant "Sam" may invoke `read_website`, `get_seo_metrics`, and `research_keywords`. These tools demonstrate actual OpenSEO capabilities rather than delivering generic SEO advice.

### Why does OpenSEO use a Durable Object for onboarding chat?

The `OnboardingChatAgent` extends `AIChatAgent` as a Durable Object to maintain **stateful conversation persistence** across HTTP requests. SQLite storage inside the Durable Object guarantees messages survive connection drops, with `persistMessages` implementing retry logic for transient failures.