Cedar provides a flexible storage system for managing chat messages with automatic persistence and automatic thread management. By default, Cedar uses no storage - no persistence unless you configure a storage adapter.

Quick Start

Cedar works out of the box with no storage (no persistence). To enable persistence, configure a storage adapter.

Storage Options

Cedar supports three storage adapters:
  1. Local Storage (default) - Browser localStorage
  2. No Storage - Disables persistence
  3. Custom Storage - Your own implementation
Configure storage by passing the messageStorage prop to CedarCopilot:
import { CedarCopilot } from 'cedar-os';

function App() {
	return (
		<CedarCopilot
			// No storage prop needed - uses local storage by default.
			// Optionally pass in user ID
			userId='user-123'>
			<YourChatComponent />
		</CedarCopilot>
	);
}

Additional Configuration Requirements by Storage Type

Local Storage & No Storage

No additional configuration needed - these options work out of the box.

Custom Storage

Implement your own storage solution by providing a custom adapter:
interface BaseStorageAdapter {
	// Required methods
	loadMessages(
		userId: string | null | undefined,
		threadId: string
	): Promise<Message[]>;
	persistMessage(
		userId: string | null | undefined,
		threadId: string,
		message: Message
	): Promise<Message>; // returns the saved message

	// Optional thread methods
	listThreads?(userId?: string | null): Promise<ThreadMeta[]>;
	createThread?(
		userId: string | null | undefined,
		threadId: string,
		meta: ThreadMeta
	): Promise<ThreadMeta>; // returns new meta
	updateThread?(
		userId: string | null | undefined,
		threadId: string,
		meta: ThreadMeta
	): Promise<ThreadMeta>; // returns updated meta
	deleteThread?(
		userId: string | null | undefined,
		threadId: string
	): Promise<ThreadMeta | undefined>; // returns deleted meta (optional)

	// Optional message methods
	updateMessage?(
		userId: string | null | undefined,
		threadId: string,
		message: Message
	): Promise<Message>; // returns updated message
	deleteMessage?(
		userId: string | null | undefined,
		threadId: string,
		messageId: string
	): Promise<Message | undefined>; // returns deleted message (optional)
}

How Default Message Storage Works

Cedar automatically handles message storage operations in the following way: Thread & Message Loading:
  • When user ID changes: Automatically loads all threads for that user
  • When storage adapter changes: Loads threads from the new storage system
  • When the thread ID changes: Automatically loads in all messages for that thread
Message Persistence & Thread Updates:
  • User messages: Persisted immediately when sent via sendMessage() (internally calls store.persistMessage).
  • Assistant responses:
    • Non-streaming: Persisted right after the LLM reply finishes.
    • Streaming: Buffered during streaming and persisted once when the stream ends.
  • Automatic thread meta update: Every persisted message refreshes the thread’s updatedAt and lastMessage fields. The thread title is always the very first message in the thread and is never overwritten.

User and Thread Management

Setting User ID: User ID is treated internally as just another Cedar State. If at runtime you want to override the user ID when using callLLM, it is also available on the SendMessageParams type
import { useCedarStore } from 'cedar-os';

function LoginHandler({ userId }: { userId: string }) {
	const store = useCedarStore();

	// Set user context - Cedar will load this user's threads
	store.getState().setCedarState('userId', userId);

	return <YourChatComponent />;
}
Setting Thread ID: By default, Cedar will use the thread ID stored in the store. If at runtime you want to override the thread ID when using callLLM, it is also available on the SendMessageParams type
import { useCedarStore } from 'cedar-os';

function ConversationSwitcher() {
	const store = useCedarStore();

	const switchToThread = (threadId: string) => {
		// Cedar will save current thread and load the new one
		store.getState().setCurrentThreadId(threadId);
	};

	return (
		<div>
			<button onClick={() => switchToThread('project-discussion')}>
				Project Discussion
			</button>
			<button onClick={() => switchToThread('support-ticket')}>
				Support Ticket
			</button>
		</div>
	);
}

Error Handling

Cedar’s storage system is designed to fail gracefully:
  • Storage failures are silently caught to prevent UI crashes
  • Failed loads return empty arrays rather than throwing errors
  • Malformed data is handled with safe fallbacks

Next Steps