Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
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.