Persistence stores the conversation outside process memory. A reload, or the same user on another device, then loads that conversation.
The server saves each thread, and useChat loads it back when the page opens.
This setup keeps the chat on the server. It also lets a reply that was still streaming finish after a reload. It takes about five minutes.
npm i @tanstack/ai-persistencepnpm add @tanstack/ai-persistenceyarn add @tanstack/ai-persistencebun add @tanstack/ai-persistenceThe client half ships in the framework package you already use.
Make one object for the whole app. memoryPersistence() keeps everything in process memory, which is good for local development:
// persistence.ts
import { memoryPersistence } from '@tanstack/ai-persistence'
export const persistence = memoryPersistence()Add withPersistence to your chat POST. Then add a GET on the same route:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
memoryStream,
resumeServerSentEventsResponse,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { reconstructChat, withPersistence } from '@tanstack/ai-persistence'
import { persistence } from './persistence'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream, {
durability: { adapter: memoryStream(request) },
})
}
export function GET(request: Request): Response | Promise<Response> {
const durability = memoryStream(request)
// A reply is still streaming: replay it from the log.
if (durability.resumeFrom() !== null) {
return resumeServerSentEventsResponse({ adapter: durability })
}
// Otherwise send back the stored thread.
return reconstructChat(persistence, request, {
// Without this check, anyone who guesses a thread id can read the thread.
authorize: async (threadId, req) => ownsThread(req, threadId),
})
}
async function ownsThread(request: Request, threadId: string): Promise<boolean> {
void request
void threadId
return true // Replace with your session and ownership check.
}Give useChat a stable threadId and set persistence: true:
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat() {
const { messages, sendMessage } = useChat({
threadId: 'support-chat',
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>{message.role}</div>
))}
<button type="button" onClick={() => void sendMessage('hi')}>
Send
</button>
</div>
)
}Send a message, then reload the page. The conversation comes back. Reload while a reply streams, and the reply finishes in place.
Tip
Your coding assistant can do this setup for you. After you install the package, run npx @tanstack/intent@latest install to add this package's Agent Skills. Then ask it to "add chat persistence to this app".
If you do not want a server store, give useChat a browser storage adapter. You do not need steps 1, 2, or 3:
import {
fetchServerSentEvents,
localStoragePersistence,
useChat,
} from '@tanstack/ai-react'
export function Chat() {
const { messages } = useChat({
threadId: 'support-chat',
connection: fetchServerSentEvents('/api/chat'),
persistence: localStoragePersistence(),
})
return <p>{messages.length} messages</p>
}This keeps the chat on one device only. Client persistence compares the two modes and the storage adapters.
Before you go to production, do these two things:
| You want | Use |
|---|---|
| The chat to survive a reload on one device | A browser storage adapter |
| The same chat on another device, or after a server restart | The quick setup |
| A reload in the middle of a reply to finish the reply | The quick setup |
| A dropped connection to resume while the page stays open | Resumable Streams alone |
| A human approval that can wait for days | The quick setup with an interrupts store. See Chat persistence |
| Sandbox files to come back after the provider sandbox is gone | Keep Files After Reload |
The same idea works outside chat:
When something surprises you, read How persistence works. It explains threads, runs, who owns the history, and the middleware lifecycle.