- 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
Build Your Own Adapter
A custom persistence adapter writes chat state to your database. Implement the stores on this page.
You do not need one. An adapter is a plain object of store functions, and the core never reads your tables. Here is the smallest adapter that works, in about 10 minutes.
Quick setup#
Install the package:
shellnpm i @tanstack/ai-persistenceshellpnpm add @tanstack/ai-persistenceshellyarn add @tanstack/ai-persistenceshellbun add @tanstack/ai-persistenceWrite a messages store. This one store is enough for withPersistence. Replace db with your own database handle:
ts// persistence.ts import { defineAIPersistence, defineMessageStore, } from '@tanstack/ai-persistence' import { db } from './db' export const persistence = defineAIPersistence({ stores: { messages: defineMessageStore({ // Return [] for a thread that was never saved, never null. loadThread: (threadId) => db.threads.messages(threadId), // The full transcript, not a delta. Overwrite what you had. saveThread: (threadId, messages) => db.threads.save(threadId, messages), }), }, })Pass it to withPersistence in your chat route:
tsimport { chat, chatParamsFromRequest, toServerSentEventsResponse, } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { 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, middleware: [withPersistence(persistence)], }) return toServerSentEventsResponse(stream) }Prove the store with the conformance suite. Run it against a test database. List the stores that you do not provide in skip:
ts// persistence.test.ts import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit' // Point `db` at a test database before you run this file. import { persistence } from './persistence' runPersistenceConformance('my adapter', () => persistence, { skip: [ 'runs', 'interrupts', 'metadata', 'activities', 'generationRuns', 'artifacts', 'blobs', ], })
Send a message, then load the thread from your database. The transcript is there.
How it works#
- withPersistence calls loadThread at the start of a run. It merges the stored thread with the incoming messages.
- It calls saveThread at the start of a run, at an interrupt, and when the run finishes. See Chat persistence.
- saveThread always gets the complete, merged transcript. Your store overwrites the row. It never appends.
- There is one define*Store helper per store. Each helper type-checks your object inline, so you do not write a type annotation.
- The schema stays yours. The core never reads a column or a table name.
Which stores do you need?#
Each store turns on one capability. Find the row for the feature you want and implement the stores in it:
| You want | Stores |
|---|---|
| Save the transcript | messages |
| Rejoin a run after a reload | messages + runs |
| Durable approvals | messages + runs + interrupts |
| App key/value state | messages + metadata |
| Persist generation runs | generationRuns |
| Keep generated files | generationRuns + artifacts + blobs |
| Rebuild sandbox files | messages + artifacts + blobs |
- Rows stack. Chat plus sandbox files means messages + artifacts + blobs. Durable approvals plus generated files means the union of both rows.
- Two pairs stay together. interrupts needs runs, and artifacts needs blobs.
- Generation runs need no chat store. See Generation persistence.
- Sandbox files also need a checkpoint store. That store lives in @tanstack/ai-sandbox, not in this table. See Keep Files After Reload.
The usual production shape is messages + runs + interrupts. Add artifacts and blobs when you keep generated files or rebuild sandbox files.
Then build each group:
- Build a chat adapter: messages, runs, interrupts, and metadata, method by method, on SQLite.
- Build a generation adapter: generationRuns, artifacts, and blobs.
- Build a sandbox adapter: the sandbox instance store, and the fields a durable sandboxed run adds to runs. Read it only if you run sandboxes.
Third-party adapters#
These adapters are published and maintained outside TanStack AI. Each one returns an AIPersistence for withPersistence, so the rest of this page applies to them too.
| Adapter | Package | Backend | Stores |
|---|---|---|---|
| Upstash | @upstash/agentkit-tanstack-ai/persistence | Upstash Redis, plus Upstash Blob for blobs | All seven |
Do you maintain an adapter? Add a row with a pull request. Run it against the conformance suite first, and list the stores that it passes.
Let your agent write it#
@tanstack/ai-persistence ships Agent Skills. They turn this page into a recipe for your stack: your ORM configuration, your schema file, and your database handle.
Install the skills into your coding assistant. Run this after the package is installed:
shellnpx @tanstack/intent@latest installshellpnpx @tanstack/intent@latest installshellyarn dlx @tanstack/intent@latest installshellbunx @tanstack/intent@latest installAsk your assistant to "add chat persistence to this app".
There are recipes for these stacks:
- Drizzle
- Prisma
- Cloudflare D1
- Anything else (raw pg, Kysely, SQLite, Mongo, Supabase)
The skills are plain Markdown under node_modules/@tanstack/ai-persistence/skills/, so you can also read them.
Advanced#
Existing database: map the contracts onto your schema#
Nothing on this page needs new tables.
- Name your columns as you like.
- Use your native types (jsonb, timestamptz) and convert them inside the store functions.
- Keep extra columns, such as user_id or audit timestamps, nullable or defaulted. The stores never write a column that they do not know.
Your existing migration tool owns these tables. See Migrations.
Own only part of it#
Put messages and runs in your database, and fill the rest from somewhere else with composePersistence:
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'
// Your own MessageStore and RunStore objects.
import { messages, runs } from './my-postgres-stores'
export const persistence = composePersistence(memoryPersistence(), {
overrides: { messages, runs },
})There is no transaction across the two systems, so a write that touches both is two writes. The store invariants (idempotent creates, insert-if-absent) make a retry of those writes safe. See Controls for every override rule.
Verify with the conformance suite#
Do not check a store by eye. Every packaged backend runs this suite, and it ships for yours too. It calls every method of every store that you provide, and it tests the ordering and idempotency rules that are easy to get wrong.
import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit'
import { sqlitePersistence } from './sqlite-persistence'
runPersistenceConformance('my sqlite adapter', () =>
sqlitePersistence({ url: ':memory:', migrate: true }),
)Declare what you left out:
- A store that you do not provide goes in skip.
- An optional runs method that you do not implement goes in skipMethods.
import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit'
import { chatOnlyPersistence } from './chat-only'
runPersistenceConformance('chat-only adapter', () => chatOnlyPersistence(), {
skip: ['activities', 'generationRuns', 'artifacts', 'blobs'],
skipMethods: ['runs.listByThread'],
})The optional runs methods are listByThread, listByParentRun, and listReclaimable.
- If you omit listByThread or listReclaimable, add it to skipMethods.
- If you omit listByParentRun, add nothing. The subagent checks skip on their own.
A store or method that is absent and not declared fails, with a message that names what to add. A half-wired adapter cannot report a pass. When the suite is green, your adapter works with withPersistence. With the generation stores, it also works with withGenerationPersistence.
Turn on the opt-in checks#
Some checks are off by default, so an adapter that passed before still passes. Turn them on to prove that your adapter supports run timings on reload (reconstructChat with includeRuns: true):
import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit'
import { sqlitePersistence } from './sqlite-persistence'
runPersistenceConformance(
'my sqlite adapter',
() => sqlitePersistence({ url: ':memory:', migrate: true }),
{ checks: ['messages.metadata', 'runs.listByThread.state'] },
)- messages.metadata: loadThread returns the metadata of each message as it was saved.
- runs.listByThread.state: listByThread returns the current status and finishedAt of each run after update.
A check that you do not turn on shows as skipped, with the option to add.
Reference#
- Store reference: every signature and invariant, and how the records relate.
- Controls: compose stores from different systems.
- Migrations: who owns the schema.
- Keep Files After Reload: use this adapter for portable snapshots. You need messages, artifacts, and blobs.