- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Persistence Overview
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.
Quick setup#
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.
1. Install the package#
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.
2. Create the persistence object#
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()3. Save and load on the server#
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.
}4. Turn it on in the client#
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".
Keep the chat in the browser instead#
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.
How it works#
- withPersistence saves the transcript, the run status, and any pending approvals to your stores.
- persistence: true tells useChat that the server owns the history. On mount, useChat calls your GET with the threadId.
- reconstructChat returns the stored messages. If a run is still streaming, it also returns that run. Then the client replays the run from the durability log.
- A second device follows the same path, because the threadId is the only key.
Before you go to production, do these two things:
- Replace memoryPersistence() with an adapter for your database. See Build your own adapter.
- Replace memoryStream with a log that all your server processes share. See Resumable streams.
Which setup do I want?#
| 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 |
Generation and sandboxes#
The same idea works outside chat:
- Image, video, speech, and transcription. The generation hooks take persistence: true too. See Generation persistence. To keep the files after the provider links expire, see Keep generated files.
- Sandboxed agents. A run can outlive the tab, and another host can adopt it. See Build a Sandbox Adapter for what to store, and Durable Runs for the reasons.
When something surprises you, read How persistence works. It explains threads, runs, who owns the history, and the middleware lifecycle.