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.
Install the package:
npm i @tanstack/ai-persistencepnpm add @tanstack/ai-persistenceyarn add @tanstack/ai-persistencebun add @tanstack/ai-persistenceWrite a messages store. This one store is enough for withPersistence. Replace db with your own database handle:
// 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:
import {
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:
// 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.
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 |
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:
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.
@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:
npx @tanstack/intent@latest installpnpx @tanstack/intent@latest installyarn dlx @tanstack/intent@latest installbunx @tanstack/intent@latest installAsk your assistant to "add chat persistence to this app".
There are recipes for these stacks:
The skills are plain Markdown under node_modules/@tanstack/ai-persistence/skills/, so you can also read them.
Nothing on this page needs new tables.
Your existing migration tool owns these tables. See Migrations.
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.
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:
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.
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.
The session stores (log and sessions) are opt-in too. Their cases run only when your persistence has the store, so a backend without them needs no skip entry. See the session stores for their rules.
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'] },
)A check that you do not turn on shows as skipped, with the option to add.