# Database hydration
Store AI conversation history in your own database. Database hydration reconciles the history you have stored with the live AI Transport session.
Database hydration is the pattern of treating your own database as the durable record of a conversation and reconciling it with the live session each time the conversation loads. Use it when your application already stores conversation history for its own features, such as search, analytics, or audit, or when conversations need to stay available longer than your retention window.
You keep the full AI Transport session for everything live. Resumable token streaming, multi-device continuity, automatic reconnection, and bidirectional control all still run over the session; database hydration changes only where long-term history is stored.
> Database hydration needs a durable session, because the session view reconciles your stored history against the channel.
Your agent persists the messages for each completed run to your database when the run completes. When a client or the agent later loads the conversation, it seeds from the database and then loads from the session only the messages newer than the last one it stored (the join point), joining the two into a single conversation with no gaps and no duplicate messages.
On the client, hydration is a single call. Give `useMessagesWithSeed` your session view and the stored seed, and it reconciles them with the live session:
#### Javascript
```
const messages = useMessagesWithSeed({ view: session.view, seed, getMessageId: (m) => m.id });
```
Hydration has two mirrored sides: the agent rebuilds the model context, and the client rebuilds the UI. Wire up the agent first, then the client.
## Hydrate the agent
The agent rebuilds the model context from your store and the live session. Seed the earlier conversation from your store, take the newest stored id as the join point, and call `run.view.loadUntil` to fetch the not-yet-stored tail. The view does the paging itself, which also merges in this invocation's triggering input message from channel history:
### Javascript
```
const session = createAgentSession({ client: ably, channelName: invocation.sessionName });
await session.connect();
const run = session.createRun(invocation, {}, { signal: req.signal });
// Seed the prior conversation from your store; the newest stored message is the seam.
const seed = loadMessages(invocation.sessionName);
const seamId = seed.at(-1)?.id;
// loadUntil pages run.view back to the seam and returns only the messages newer
// than it (the not-yet-stored tail). It drives the paging itself, which also
// folds in this invocation's triggering input from channel history.
const tail = await run.view.loadUntil((m) => m.message.id === seamId);
await run.start();
const conversation = [...seed, ...tail.map((m) => m.message)];
// ...stream the model response with `conversation` as the message history...
```
## Persist the completed run
Persist a run once it completes. While a run is in flight its messages are still changing: tokens append, tool calls resolve, an assistant message grows. Once the run reaches a terminal state the agent publishes no more output for it, so it is the natural atomic unit to write to your store, and a write keyed by `message.id` is safe to repeat.
On the agent, after the model response streams, persist the run's messages. `run.messages` is a `BaseRun` accessor that returns the run's entire contribution: its triggering input plus all of its streamed output, even when the run suspends and resumes. A turn that suspends for a client-side tool result and resumes under the same run therefore still persists as one lossless unit.
### Javascript
```
const runMessages = run.messages;
await run.end(outcome);
if (outcome.reason === 'complete') await appendMessages(invocation.sessionName, runMessages);
```
Every call to `send` publishes at most one new message and nothing else, so a published message may have no run against it until your application wakes the agent, and a `send` carrying a `runId` re-enters that run instead of starting one. Regenerating parents a second reply run at the same input message, so a hydration that unions runs' messages must key on `codec-message-id` to avoid repeating that shared input.
## Hydrate the client
The client reconciles the same way as the agent, over its own session view. Seed from your store, then call `session.view.loadUntil` to fetch the tail newer than the join point and compose the two:
### Javascript
```
const seed = loadStoredMessages(conversationId);
const seamId = seed.at(-1)?.id;
const tail = await session.view.loadUntil((m) => m.message.id === seamId);
const conversation = [...seed, ...tail.map((m) => m.message)];
```
In React, `useMessagesWithSeed` wraps that walk. Pass your `session.view` and the stored seed, and it returns the composed conversation, kept current as new messages stream in:
### Javascript
```
const messages = useMessagesWithSeed({ view: session.view, seed, getMessageId: (m) => m.id });
```
If you use the Vercel AI SDK's `useChat`, [`useMessageSync`](https://ably.com/docs/ai-transport/api/react/vercel/use-message-sync.md) runs the same reconciliation against `useChat`'s message state from a `messages` seed, so you keep hydration without leaving `useChat`.
## How reconciliation works
Both sides reconcile against the same join point: the newest message your store already holds. Its domain `message.id` is the only id shared by both sides. The last message you persisted carries the same id on the channel, because the transport's internal `codecMessageId` is never persisted.
`View.loadUntil(predicate, signal?)` pages the view backward through `loadOlder` until a message matches the predicate, then returns only the messages strictly newer than it. The join point is an exclusive floor: the matched message is not returned, because your store already holds it, so composing `[...seed, ...tail]` drops exactly one overlap and leaves no gap.
When the store is empty there is no join point. The predicate never matches, so `loadUntil` pages the whole conversation, and hydration behaves exactly like loading history from the session.
Reconciliation relies on the conversation being linear: each run is persisted whole before the next is sent. Nothing in the SDK enforces this. A call to `send` while a run is active either steers that run, when it carries the live `runId`, or starts a second one, so serialising runs is your agent's job.
Two preconditions come with that:
- Your agent has to be the only writer to the store. A second writer can put a message between the join point and the tail that neither side accounts for.
- Only one paginator may read a given `session.view`. A second one, such as a `useView` paging past the same join point, reintroduces the duplicate the join point exists to drop.
If the join point is not present on the channel at all, which happens once your retention window has dropped it, the predicate never matches, `loadUntil` returns the whole window it paged, and the conversation still hydrates.
## Bring a store you already have
If you arrive here from [streaming](https://ably.com/docs/ai-transport/streaming.md), you already have a conversation table and a hydration endpoint, and both were the source of truth. Adopting a durable session changes that: the channel becomes the source of truth for everything live, and your store becomes the long-term record behind it.
What that means in practice:
- Your read path changes. The client stops fetching the conversation from your endpoint and renders from the session view instead, seeded from your store through `useMessagesWithSeed`.
- Your write path narrows. Instead of writing every message as it arrives, the agent persists `run.messages` once, when a run completes.
- Your existing rows become the seed. They need no migration as long as each carries the domain `message.id`, which is the join key both sides share.
- The session can hold everything on its own. If your channel's retention window covers your conversations, drop the store and let the session hold everything, and [history and replay](https://ably.com/docs/ai-transport/streaming/history.md) is then the whole read path.
## FAQ
### Do I need a database?
Only to retain conversations beyond your Ably retention window. While your retention window still covers the conversation, the session is the source of truth and [history and replay](https://ably.com/docs/ai-transport/streaming/history.md) loads the whole conversation from the session on its own. Add a database when you need conversations to outlive your retention window.
### What do I persist, the whole conversation or just the latest run?
Persist each completed run's messages, which is `run.messages`: the run's triggering input plus its streamed output. The union of all runs reconstructs the whole conversation, so you never re-serialise the entire conversation on each run.
### What if the store is behind the session?
That is the expected state on reload. The join point is the newest message your store holds, and `loadUntil` returns the session messages strictly newer than it. Composing the seed with that tail fills the gap, so a store that lags the session by several runs still produces a gapless conversation.
### How does this differ from scroll-back history?
Scroll-back through [history and replay](https://ably.com/docs/ai-transport/streaming/history.md) pages the session backward inside your retention window and never touches a database. Database hydration seeds the conversation from your own store and reconciles the store with the session at the join point, so it covers the messages your retention window no longer holds.
### Why is the domain id the join point and not `codecMessageId`?
`codecMessageId` is the transport's internal id and is never persisted, so it is not available in your store. The domain `message.id` is the id you control and persist, and the same id appears on the channel, which makes it the only stable join key between the two sides.
## Related features
- [History and replay](https://ably.com/docs/ai-transport/streaming/history.md): load and paginate conversation history from the session within your retention window.
- [Multi-device and fan-out](https://ably.com/docs/ai-transport/streaming/multi-device.md): how the same hydrated conversation appears across a user's devices.
- [`useMessagesWithSeed`](https://ably.com/docs/ai-transport/api/react/core/use-messages-with-seed.md): the core React hook that composes a seed with the live view.
- [`useMessageSync`](https://ably.com/docs/ai-transport/api/react/vercel/use-message-sync.md): the Vercel hook that reconciles a `useChat` seed with the session.
## Related Topics
- [Overview](https://ably.com/docs/ai-transport/durable-sessions.md): A drop-in durable session layer for AI applications. AI Transport holds the conversation and the message state your UI renders, including branching, and you render from its React hooks.
- [Sessions](https://ably.com/docs/ai-transport/durable-sessions/sessions.md): Understand sessions in AI Transport: persistent, shared conversation state that exists independently of any connection, and the ClientSession and AgentSession objects that attach to it.
- [Conversation tree](https://ably.com/docs/ai-transport/durable-sessions/conversation-tree.md): Understand how AI Transport organises messages into a branching conversation tree, and how views give each client its own linear path through it.
- [Optimistic updates](https://ably.com/docs/ai-transport/durable-sessions/optimistic-updates.md): User messages appear instantly in Ably AI Transport. Optimistic insertion with automatic reconciliation when the server confirms.
- [Branching, edit, and regenerate](https://ably.com/docs/ai-transport/durable-sessions/branching.md): Edit user messages, regenerate AI responses, and navigate branches with Ably AI Transport. The full history is preserved in the conversation tree.
- [Tool calling](https://ably.com/docs/ai-transport/durable-sessions/tool-calling.md): Stream tool invocations and results through Ably AI Transport. Server-executed and client-executed tools with persistent state.
- [Human-in-the-loop](https://ably.com/docs/ai-transport/durable-sessions/human-in-the-loop.md): Add human approval gates to AI agent workflows with Ably AI Transport. Approve tool executions and provide input across devices.
- [Migrate from Streaming](https://ably.com/docs/ai-transport/durable-sessions/move-up-from-streaming.md): What changes in an application already streaming with Ably AI Transport when it moves the conversation into a durable session, and what stays exactly as it is.
## Documentation Index
To discover additional Ably documentation:
1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages.
2. Identify relevant URLs from that index.
3. Fetch target pages as needed.
Avoid using assumed or outdated documentation paths.