---
title: "ChatStore"
description: "The user-bound persistence contract, its ownership invariants, pagination options, and usage record seam."
---

`ChatStore` is the persistence contract used by `createChatHandler`. A factory is
called after authentication and returns an instance bound to one verified user.

```ts
import type { ChatStore, ChatStoreFactory } from '@mordn/chat-widget/server';

type ChatStoreFactory = (userId: string) => ChatStore;
```

> **Keep the user out of method arguments**
>
> No method accepts a `userId`. Every implementation must scope reads and writes
> to the bound identity. Never reconstruct identity from request input inside a
> store method.

## Interface

```ts
interface ChatStore {
  readonly userId: string;

  listConversations(): Promise<StoredConversation[]>;
  getConversation(id: string): Promise<StoredConversation | null>;
  ensureConversation(id: string, init?: { title?: string }): Promise<StoredConversation>;
  renameConversation(id: string, title: string): Promise<void>;
  deleteConversation(id: string): Promise<boolean>;

  listMessages(
    conversationId: string,
    options?: { limit?: number; before?: Date },
  ): Promise<StoredMessage[]>;

  saveTurn(input: SaveTurnInput): Promise<void>;
}
```

## Required invariants

- `getConversation` returns `null` for both missing and foreign conversations.
- `ensureConversation` and `saveTurn` throw `ConversationOwnershipError` for a
  foreign conversation id.
- `listMessages` never returns another user's messages.
- `saveTurn` is idempotent on message id and persists the complete `parts` array.
- Implementations should clamp requested page sizes. The Drizzle store caps
  message pages at 100 and supports `before` pagination.
- `deleteConversation` removes only the bound user's row. Attachment blob purge
  is coordinated by the handler through the storage adapter.

> **Warning**
>
> Capabilities vary by adapter. The current hosted store ignores
> `ListMessagesOptions` and fetches the complete conversation, so `limit` and
> `before` pagination are Drizzle/custom-store behavior until hosted pagination is
> implemented.

## Core types

```ts
interface StoredConversation {
  id: string;
  title: string;
  metadata: Record<string, unknown> | null;
  createdAt: Date;
  updatedAt: Date;
  messageCount?: number;
}

interface StoredMessage {
  id: string;
  role: 'user' | 'assistant' | 'system';
  parts: UIMessage['parts'];
  text: string;
  model?: string;
  createdAt: Date;
}

interface SaveTurnInput {
  conversationId: string;
  messages: UIMessage[];
  model?: string;
  usage?: UsageRecord;
}
```

Stores may persist `usage`; an implementation that does not support usage may
ignore it. Usage accounting must never compromise message ownership or
persistence.

## Custom implementations

Treat the method contracts as security requirements. See
[Custom store & adapter](/docs/backends/custom) for an implementation skeleton
and [Security model](/docs/security) for the threat model.
