# Add Persistent Chat History to the Vercel AI SDK

> A working example that persists a Vercel AI SDK chat, reloads it after a restart, and adds search and memory over the same messages.

Canonical: https://dialoguedb.com/blog/add-persistent-chat-history-vercel-ai-sdk
Published: 2026-07-10
Updated: 2026-07-15

Conversation Persistence & Integrations

# Add Persistent Chat History to the Vercel AI SDK

July 10, 2026

![Add Persistent Chat History to the Vercel AI SDK](https://dialoguedb.com/images/add-persistent-chat-history-vercel-ai-sdk-feature-image.jpg)

[ Back to Blog](https://dialoguedb.com/blog)

A Vercel AI SDK app streams responses and renders the chat UI, but it doesn’t keep the conversation. The messages live in browser state and in the request that’s currently running, so a reload or a redeploy leaves the user with an empty thread. Making chat history persistent means moving those messages somewhere durable, so they outlast the browser tab and the server process.

Doing that takes two things: saving each turn as it finishes, and loading the conversation back when the user returns. The catch specific to the AI SDK is the shape of a message. Each one isn’t a plain string but a set of structured parts (text, tool calls, reasoning), and the store has to keep those parts intact and return them in the same shape, or the reloaded conversation won’t replay correctly.

All of it comes down to a mapping between the two message formats, plus a save and a load. This post walks through a small `useChat` app wired to DialogueDB that already handles it: every turn is saved the moment the model finishes, and the full conversation reloads after a restart, so history survives a refresh, a redeploy, or a user coming back days later. Clone it, run it locally, and the same handful of lines drops straight into your own Route Handler. And because that history now lives in DialogueDB, the same conversation is instantly ready for semantic search and cross-session memory, with no second or third datastore to bolt on.

## What persistent chat history adds to a Vercel AI SDK app

Once the messages live somewhere durable, you get more than a thread that survives a refresh. The example is a small Next.js reference with three moving parts: a Route Handler that streams and saves replies, a client that loads the conversation on mount, and the one file that maps between the two message formats.

How a turn round-trips

Saving a turn

Browser useChat

messages

Route Handler onFinish

saveMessages

DialogueDB stored history

 refresh, redeploy, or cold start

Reloading the conversation

DialogueDB stored history

loadUIMessages

Route Handler validateUIMessages

messages

Browser useChat, rehydrated

A refresh or redeploy clears useChat's in-memory history. DialogueDB keeps it, so the reload path rebuilds the same conversation, tool calls and all.

Every turn is written to storage as it finishes, and on load the conversation is read back into `useChat`. A refresh or a redeploy clears the browser’s copy of the chat, and the reload path rebuilds it from storage, tool calls and all.

Because the messages live in DialogueDB rather than a flat table, the same data supports two more things:

- **[Search across past turns by meaning](https://dialoguedb.com/architecture/auto-vectorization).** Find where a topic came up in a long conversation without matching exact words.
- **[Memory that carries between sessions](https://dialoguedb.com/architecture/conversation-memory).** Keep facts about a user separate from the message list, and read them back on the next visit.

## How to save and reload chat history

There are two message shapes, and the integration is the mapping between them. A [useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) message is `{ id, role, parts }`. A stored message is `{ id, role, content }`. Store the `parts` array as `content` on the way in, and on the way out parse the rows back with `validateUIMessages`, which Vercel’s own [message persistence guide](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence) recommends. The parts stay structured throughout, so text, tool calls, and reasoning come back in the same shape they went in.

```typescript
// persist.ts
import { validateUIMessages, type UIMessage } from 'ai'
import type { DialogueDB, MessageContent } from 'dialogue-db'

export function toStoredMessages(
  messages: UIMessage[],
): { role: string; content: MessageContent }[] {
  return messages.map((message) => ({
    role: message.role,
    content: message.parts,
  }))
}

export async function loadUIMessages(
  db: DialogueDB,
  id: string,
  namespace: string,
): Promise<UIMessage[]> {
  const dialogue = await db.getDialogue(id, { namespace })
  if (!dialogue) return []
  await dialogue.loadMessages({ order: 'asc' })
  return validateUIMessages({
    messages: dialogue.messages.map((m) => ({
      id: m.id,
      role: m.role,
      parts: m.content,
    })),
  })
}

```

The Route Handler wires that into a normal streaming response. It saves the incoming message, streams the model, and saves the reply when the turn finishes. See how each step maps to the code that runs it, hover any step to trace it:

1 Get or create the dialogue One conversation, scoped to the user through its namespace. 2 Save the incoming user turn Only the last message is written, not the whole history. 3 Stream the model streamText runs on the full history, exactly as before. 4 Save the reply in onFinish The completed assistant message(s) persist once the turn ends.

app/api/chat/route.ts

import { streamText, convertToModelMessages } from "ai"

import { openai } from "@ai-sdk/openai"

import { DialogueDB } from "dialogue-db"

import { toStoredMessages } from "@/lib/persist"

const db \= new DialogueDB ()

export async function POST (req: Request) {

const { messages, dialogueId, userId } \= await req.json ()

const dialogue \= await db.getOrCreateDialogue ({

id: dialogueId,

namespace: userId,

})

// persist the incoming user turn

await dialogue.saveMessages (

toStoredMessages (\[messages\[messages.length - 1\]\]),

)

const result \= streamText ({

model: openai ( "gpt-4o" ),

messages: await convertToModelMessages (messages),

})

return result.toUIMessageStreamResponse ({

originalMessages: messages,

onFinish: async ({ messages: updated }) => {

// persist the new assistant message(s)

await dialogue.saveMessages (

toStoredMessages (updated.slice (messages.length)),

)

},

})

}

Hover a step to see the lines it maps to.

On load, the client reads the stored conversation and hands it to `useChat` through its `messages` prop, so the UI renders the history with no special-casing. To reproduce this in your own app, you drop those same two calls (the save when a turn finishes and the load on mount) into your Route Handler.

### See the full Vercel AI SDK integration

An interactive walkthrough of the whole flow, from the client side to memory and semantic search.

[See the integration](https://dialoguedb.com/integrations/vercel-ai-sdk)

## Why use a conversation database?

The example is wired to DialogueDB, the conversation database at the center of this whole pattern. One client stores the messages and, over that same data, adds memory, semantic search, threads, and [per-user isolation](https://dialoguedb.com/architecture/multi-tenant), all under one namespace and one API key. The chat history you just persisted is already everything those capabilities run on.

That breadth from a single client is the real reason to reach for DialogueDB here. On most stacks these are three separate systems to run and keep in sync: one to persist messages, a second to hold memory that survives between sessions, and a third (usually a vector database) to search past turns by meaning. DialogueDB collapses all three into one client:

- Messages, memory, and search share a namespace, so there are no user IDs to keep aligned across separate stores.
- Message content is stored as structured parts, so tool calls and reasoning replay exactly.
- Semantic search is built in, so there’s no separate vector database to run or keep in sync.
- The client is HTTP-only with no connection pool, so it runs on the Vercel Edge runtime and standard serverless.

You also decide what gets remembered. There is no background process reading conversations and extracting facts; you call `createMemory` with the value you want to keep.

## Postgres vs DialogueDB for chat history

You don’t strictly need DialogueDB to persist chat history, since a plain Postgres table works fine for the storage itself. The difference is how much you have to assemble yourself to match what the example already does out of the box.

| You handle                    | Postgres + pgvector         | DialogueDB                  |
| ----------------------------- | --------------------------- | --------------------------- |
| Message schema and migrations | Design and maintain         | Provided                    |
| Structured parts (tool calls) | Serialize and rebuild       | Stored as parts             |
| Search across turns           | Add and sync a vector store | Built in                    |
| Loading a conversation        | Write the query             | getDialogue \+ loadMessages |
| Per-user isolation            | Query code on every read    | namespace on every call     |
| Memory across sessions        | A second store to build     | Same client                 |

Because DialogueDB is managed, it runs and scales the storage for you instead of you standing up a database and keeping it healthy. In exchange, messages, memory, and search all come from one integration instead of three, with no vector database or sync code to maintain. If you are weighing the storage options more broadly, [How to Store AI Chat History](https://dialoguedb.com/blog/how-to-store-ai-chat-history) covers the tradeoffs, and [Conversation Persistence for TypeScript Agent Frameworks](https://dialoguedb.com/blog/conversation-persistence-typescript-agent-frameworks) looks at the message-format problem across SDKs.

## Frequently asked questions

Does DialogueDB work with useChat and streamText?

Yes, DialogueDB works with useChat and streamText. Any handler that exposes an onFinish callback can save messages there, and the loaded messages pass straight back into the useChat messages prop.

Do tool calls survive when messages are saved?

Yes, they do. DialogueDB stores the parts array as structured content, so tool calls and reasoning round-trip unchanged rather than collapsing to plain text.

Do I need a separate vector database for search?

No. Semantic search over messages and memories is part of DialogueDB, so there is no vector store to run or keep in sync alongside your chat history.

Does it run on the Vercel Edge runtime?

Yes. The client is HTTP-only with no connection pool and no native dependencies, so it also runs on Node and standard serverless without special configuration.

## Run the example

Clone the example, add your DialogueDB and OpenAI keys (OpenAI is just the model the example calls; the persistence pattern works with any AI SDK provider), and start it:

```bash
git clone https://github.com/dialoguedb/examples
cd examples/vercel-ai-sdk
npm install
cp .env.example .env
npm run dev

```

You will see turn one, a cold reload from DialogueDB, then turn two continued from the reloaded history. From there, the same mapping and save calls drop into a real Next.js Route Handler. The [integration page](https://dialoguedb.com/integrations/vercel-ai-sdk) walks through the client side and the full request flow, and the [quickstart](https://docs.dialoguedb.com/guide/quickstart) gets you an API key.

### Ready to Build Better Conversations?

Get started with DialogueDB in minutes. Free tier included.

[Get Your API Key](https://app.dialoguedb.com/signup)

### Share

[](https://x.com/intent/tweet?url=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fadd-persistent-chat-history-vercel-ai-sdk&text=Add%20Persistent%20Chat%20History%20to%20the%20Vercel%20AI%20SDK) [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fadd-persistent-chat-history-vercel-ai-sdk) [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fadd-persistent-chat-history-vercel-ai-sdk)

Conversation Persistence Integrations Semantic Search Agent Memory

### Latest Posts

- [Multi-Tenant Conversation Isolation with Postgres RLS and pgvector](https://dialoguedb.com/blog/postgres-rls-pgvector-multi-tenant-isolation)
- [Add Durable Session Storage to the OpenAI Agents SDK](https://dialoguedb.com/blog/durable-session-storage-openai-agents-sdk)
- [Add Persistent Chat History to the Vercel AI SDK](https://dialoguedb.com/blog/add-persistent-chat-history-vercel-ai-sdk)

[ Previous](https://dialoguedb.com/blog/durable-session-storage-openai-agents-sdk) [Next](https://dialoguedb.com/blog/mcp-stateless-conversation-persistence)
