ChatStore
ChatStore is the persistence contract used by createChatHandler. A factory is
called after authentication and returns an instance bound to one verified user.
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
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
getConversationreturnsnullfor both missing and foreign conversations.ensureConversationandsaveTurnthrowConversationOwnershipErrorfor a foreign conversation id.listMessagesnever returns another user's messages.saveTurnis idempotent on message id and persists the completepartsarray.- Implementations should clamp requested page sizes. The Drizzle store caps
message pages at 100 and supports
beforepagination. deleteConversationremoves 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
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.