ChatStore

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

code
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

code
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

code
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 for an implementation skeleton and Security model for the threat model.