> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cedarcopilot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Message Storage Configuration

> Configure how Cedar persists chat messages and manages threads

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.

<Warning>
  **Storage Requirements:** Message persistence requires both `userId` and
  `threadId` to be set. Without these IDs, storage operations are skipped and
  messages remain in memory only.
</Warning>

# 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:

<CodeGroup>
  ```typescript Local Storage (Default) theme={null}
  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>
  	);
  }
  ```

  ```typescript Local Storage (Custom Key) theme={null}
  import { CedarCopilot } from 'cedar-os';

  function App() {
  	return (
  		<CedarCopilot
  			messageStorage={{
  				type: 'local',
  				options: {
  					key: 'my-app', // Optional: defaults to 'cedar'
  				},
  			}}
  			userId='user-123'>
  			<YourChatComponent />
  		</CedarCopilot>
  	);
  }
  ```

  ```typescript No Storage theme={null}
  import { CedarCopilot } from 'cedar-os';

  function App() {
  	return (
  		<CedarCopilot messageStorage={{ type: 'none' }} userId='user-123'>
  			<YourChatComponent />
  		</CedarCopilot>
  	);
  }
  ```

  ```typescript Custom Storage theme={null}
  import { CedarCopilot } from 'cedar-os';

  const myCustomAdapter = {
  	async listThreads(userId) {
  		return await myDatabase.getThreads(userId);
  	},
  	async loadMessages(userId, threadId) {
  		return await myDatabase.getMessages(userId, threadId);
  	},
  	async persistMessage(userId, threadId, message) {
  		await myDatabase.appendMessage(userId, threadId, message);
  	},
  };

  function App() {
  	return (
  		<CedarCopilot
  			messageStorage={{
  				type: 'custom',
  				adapter: myCustomAdapter,
  			}}
  			userId='user-123'>
  			<YourChatComponent />
  		</CedarCopilot>
  	);
  }
  ```
</CodeGroup>

# 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:

<CodeGroup>
  ```typescript Interface theme={null}
  interface MessageStorageBaseAdapter {
  	// Required methods
  	loadMessages(userId: string, threadId: string): Promise<Message[]>;
  	persistMessage(
  		userId: string,
  		threadId: string,
  		message: Message
  	): Promise<Message>; // returns the saved message

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

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

  All mutator/creator functions return the object they operate on, making it easy to work with the freshly-persisted data (IDs, timestamps, etc.).

  ```typescript Example Implementation theme={null}
  import { CedarCopilot } from 'cedar-os';

  // Your custom storage implementation
  const myCustomAdapter = {
  	async listThreads(userId) {
  		return await myDatabase.getThreads(userId);
  	},

  	async loadMessages(userId, threadId) {
  		return await myDatabase.getMessages(userId, threadId);
  	},

  	async persistMessage(userId, threadId, message) {
  		await myDatabase.appendMessage(userId, threadId, message);
  	},
  };

  function App() {
  	return (
  		<CedarCopilot
  			messageStorage={{
  				type: 'custom',
  				adapter: myCustomAdapter,
  			}}
  			userId='user-123' // User ID passed in initial configuration
  		>
  			<YourChatComponent />
  		</CedarCopilot>
  	);
  }
  ```
</CodeGroup>

## How Default Message Storage Works

Cedar preconfigures several storage methods that orchestrate your adapter's functionality. Understanding these helps you work with Cedar's storage system effectively.

### Preconfigured Storage Methods

<Note>
  Cedar provides these methods out of the box. These methods are automatically
  called by Cedar at appropriate times - you don't need to call them directly.
  To customize their behavior, you can override them in your implementation.
</Note>

#### 1. `initializeChat({threadId?: string, userId?: string})`

This method orchestrates the initial loading of threads and messages. It's automatically called:

* When the message storage adapter is first set
* When userId or threadId changes in Cedar state

Here's what it does with your adapter:

```typescript theme={null}
// Cedar's initializeChat flow:
1. Get userId (from params or Cedar state)
2. Get threadId (from params or Cedar state)
3. Call adapter.listThreads(userId) to load user's threads
4. If no thread is selected and threads exist:
   - Automatically select the first thread
5. If **no threads exist** and `adapter.createThread` is available:
   - A brand-new thread is created (ID `thread-{timestamp}-{rand}`)
   - The new thread metadata is persisted via `adapter.createThread`
   - Cedar sets this new ID in state
6. Use the provided threadId OR the auto-selected / newly-created threadId
7. Clear any existing messages in the UI
8. If both userId and threadId exist:
   - Call adapter.loadMessages(userId, threadId)
   - Display the loaded messages
```

#### 2. `persistMessageStorageMessage(message)`

This method handles message persistence with intelligent thread management:

```typescript theme={null}
// Cedar's persistMessageStorageMessage flow:
1. Get userId from Cedar state (skip entire process if no userId)
2. Get threadId from Cedar state
3. Call adapter.persistMessage(userId, threadId, message)
4. If adapter.updateThread is available:
   - Update thread metadata (preserving original title if exists)
   - Set updatedAt to current timestamp
5. Reload and refresh the thread list via loadAndSelectThreads()
```

#### 3. `sendMessage()` Storage Actions

When a user sends a message, Cedar performs these storage operations:

```typescript theme={null}
// Cedar's sendMessage storage flow:
1. Add user message to UI via addMessage()
   - addMessage() automatically calls persistMessageStorageMessage()
   - This triggers the persistence flow described above
2. When assistant responds:
   - For non-streaming: Add response via addMessage() (auto-persisted)
   - For streaming:
     - Append content to messages during stream
     - Persist all new messages after stream completes
```

<Info>
  Best practice: make your **load** endpoints idempotent. Implementations of
  `listThreads` or `loadMessages` should **NOT** perform any mutations such as
  creating threads or messages. Cedar may call these functions multiple times
  during normal operation; if they create data each time you could end up with
  duplicated threads or messages.
</Info>

### Automatic Behaviors

**Thread & Message Loading:**

* **When storage adapter is set**: Adapter is configured but chat initialization happens separately
* **When user ID or thread ID changes**: Triggers `initializeChat` which:
  * Loads all threads for the user
  * May auto-select first thread if none selected
  * Loads messages for the selected thread
* **When userId is undefined**: Storage operations are skipped entirely
* **When threadId is undefined**: A new UUID is generated during message persistence

# User and Thread Management

<Info>
  User ID and thread ID are both stored as Cedar State variables and are
  automatically sent to the backend for Mastra and Custom backends. See Agent
  Backend Connection docs for more details.
</Info>

### Setting User ID

User ID is a Cedar State variable that can be set in two ways:

**Method 1**: Initial Configuration (Recommended)

Pass the user ID directly to CedarCopilot as a prop. This is the simplest and most common approach:

```typescript theme={null}
import { CedarCopilot } from 'cedar-os';

function App({ userId }: { userId: string }) {
	return (
		<CedarCopilot
			userId={userId} // Set user ID in initial configuration
			messageStorage={{
				type: 'local', // or 'none', 'custom'
			}}>
			<YourChatComponent />
		</CedarCopilot>
	);
}
```

<Note>
  When you pass `userId` to CedarCopilot, it automatically: - Registers it as a
  Cedar State variable (null values become empty strings) - Triggers
  `initializeChat` which loads threads for that user - Makes it available
  throughout your app via Cedar State
</Note>

**Method 2**: Dynamic Setting via State APIs

Use this when you need to change users at runtime (e.g., after login, user switching):

<CodeGroup>
  ```typescript Using setCedarState theme={null}
  import { setCedarState } from 'cedar-os';

  function LoginHandler() {
  	const handleLogin = async (credentials) => {
  		const user = await loginUser(credentials);

  		// Set user ID - Cedar will load this user's threads
  		setCedarState('userId', user.id);
  	};

  	return <LoginForm onSubmit={handleLogin} />;
  }
  ```

  ```typescript Using useCedarState Hook theme={null}
  import { useCedarState } from 'cedar-os';

  function UserSwitcher() {
  	const [userId, setUserId] = useCedarState('userId', null);

  	const switchUser = (newUserId: string) => {
  		// This will trigger thread reload for the new user
  		setUserId(newUserId);
  	};

  	return (
  		<select onChange={(e) => switchUser(e.target.value)}>
  			<option value='user-1'>User 1</option>
  			<option value='user-2'>User 2</option>
  		</select>
  	);
  }
  ```

  ```typescript Getting Current User theme={null}
  import { getCedarState } from 'cedar-os';

  function CurrentUserDisplay() {
  	const userId = getCedarState('userId');

  	return <div>Current user: {userId || 'Not logged in'}</div>;
  }
  ```
</CodeGroup>

When the user ID changes:

1. Cedar loads threads for the new user
2. If no thread is selected, may auto-select first thread (if `listThreads` is implemented)
3. Messages are cleared and reloaded based on the user's threads

### Setting Thread ID

Thread ID is a Cedar State variable that can be set in three ways:

**Method 1**: Initial Configuration

Pass the thread ID directly to CedarCopilot as a prop. This is useful when you want to start with a specific thread:

```typescript theme={null}
import { CedarCopilot } from 'cedar-os';

function App({ userId, threadId }: { userId: string; threadId: string }) {
	return (
		<CedarCopilot
			userId={userId}
			threadId={threadId} // Set initial thread ID
			messageStorage={{
				type: 'local',
			}}>
			<YourChatComponent />
		</CedarCopilot>
	);
}
```

<Note>
  Like userId, threadId is registered as a Cedar State variable with null values
  converted to empty strings. This triggers `initializeChat` to load messages
  for the specified thread.
</Note>

**Method 2**: Explicit Setting (User Control)

Use this when users manually select threads, navigate to specific conversations, or create new threads at runtime:

<CodeGroup>
  ```typescript Using Store API theme={null}
  import { setCedarState } from 'cedar-os';

  function ConversationSwitcher() {
  	const switchToThread = (threadId: string) => {
  		// Cedar will save current thread and load the new one
  		setCedarState('threadId', threadId);
  	};

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

  ```typescript Using Hook theme={null}
  import { useCedarState } from 'cedar-os';

  function ConversationManager() {
  	const [threadId, setThreadId] = useCedarState('threadId', null);

  	return (
  		<div>
  			<p>Current thread: {threadId || 'No thread selected'}</p>
  			<button onClick={() => setThreadId('new-thread-123')}>
  				Start New Thread
  			</button>
  		</div>
  	);
  }
  ```

  ```typescript Programmatic Control theme={null}
  // From anywhere in your application
  import { setCedarState, getCedarState } from 'cedar-os';

  // Set a specific thread
  setCedarState('threadId', 'thread-abc-123');

  // Get current thread
  const currentThread = getCedarState('threadId');

  // Navigate to a thread from URL params
  const threadFromUrl = searchParams.get('thread');
  if (threadFromUrl) {
  	setCedarState('threadId', threadFromUrl);
  }
  ```
</CodeGroup>

**Method 3**: Automatic Selection via `listThreads`

If you implement the `listThreads` method in your storage adapter, Cedar will automatically select the first available thread when no thread is currently selected. This is perfect for app initialization:

```typescript theme={null}
const myStorageAdapter = {
	async listThreads(userId) {
		// Return user's threads from your backend/database
		return [
			{
				id: 'thread-1',
				title: 'General Chat',
				updatedAt: '2024-01-20T10:00:00Z',
			},
			{
				id: 'thread-2',
				title: 'Project Discussion',
				updatedAt: '2024-01-19T15:30:00Z',
			},
		];
	},
	// ... other required methods
};

// When storage adapter is set or threads are loaded:
// - If threadId is null AND threads exist
// - Cedar automatically calls: setCedarState('threadId', threads[0].id)
// - In this example, 'thread-1' would be selected
```

When the thread ID changes (via any method), Cedar automatically:

1. Clears the current messages from the UI
2. Loads messages for the new thread from storage
3. Updates the UI with the new thread's messages

## Next Steps

* Learn about [custom message rendering](/getting-started/chat/custom-message-rendering) for rich content
* Explore [streaming responses](/getting-started/chat/streaming) for real-time interactions
* Set up [agent backend connections](/getting-started/agent-backend-connection/agent-backend-connection) for AI responses
