Skip to content
Persistence

Persistence Controls

Pass only the stores you use. Messages can live in your database while approvals live somewhere else.

Persistence has no feature flags. The stores that the backend provides decide what is persisted. Use composePersistence to mix backends per store, and give only the stores your app needs.

Swap one store

composePersistence takes the base backend first and an overrides object second. This example starts from the in-memory backend and replaces the interrupts and runs stores:

ts
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'
// Your own implementations of the InterruptStore and RunStore contracts.
import { interruptStore, runStore } from './stores'

const persistence = composePersistence(memoryPersistence(), {
  overrides: {
    interrupts: interruptStore,
    runs: runStore,
  },
})

Remove one store

Set the store to false:

ts
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'

// Drop metadata. The resulting type has no `metadata` key.
const withoutMetadata = composePersistence(memoryPersistence(), {
  overrides: { metadata: false },
})

Each override is independent:

Override valueResult
Key omittedKeep the base store.
undefinedKeep the base store.
A store objectReplace that store only.
falseRemove that store.

Unknown store names fail the type check. They also fail at runtime when the values come from untyped JavaScript.

To define a partial backend directly, without a base, use defineAIPersistence({ stores: { ... } }) and pass only the stores you have. The store reference has the store contracts.

What each store gives you

You wantStore
A server transcript that is the true copymessages (required by withPersistence and reconstructChat)
Run status and usageruns (required on ChatPersistence, and required when interrupts is set)
Durable approvals or human inputinterrupts (needs runs)
App or integration checkpointsmetadata (always optional)

withPersistence(persistence) looks at the stores that are present. A present store turns its feature on.

Valid store combinations

  • withPersistence needs messages.
  • interrupts needs runs, because an interrupt record belongs to a run.
  • withGenerationPersistence needs generationRuns.
  • Portable sandbox snapshots need messages, artifacts, and blobs on the same persistence object. See Keep Files After Reload.

Advanced

Named shapes

Use these types for a persistence object. They are better than a sparse map:

TypeRequired storesUse
ChatTranscriptStores / ChatTranscriptPersistencemessages (runs, interrupts, and metadata optional)The minimum for withPersistence and reconstructChat
ChatPersistenceStores / ChatPersistencemessages, runs, interrupts, and metadataPackaged backends (memoryPersistence, Drizzle, Prisma, D1)
ChatWithInterruptsStores / ChatWithInterruptsPersistencemessages, runs, and interruptsHuman-in-the-loop without metadata

There is no public sparse AIPersistenceStores export. For a custom map, use a named shape or AIPersistence<{ messages: MessageStore, ... }>. defineAIPersistence and composePersistence still accept sparse maps by inference.

Entrypoint requirements

EntrypointShapeNotes
withPersistenceAt least ChatTranscriptStoresinterrupts needs runs
reconstructChatChatTranscriptStoresruns and interrupts add data to the response when present
Packaged *Persistence()ChatPersistencemessages and runs, plus interrupts and metadata
defineAIPersistence / composePersistenceSparse, by inferenceUse a named shape for the result

Locks

Locks coordinate work across server instances, like a distributed mutex. They live in @tanstack/ai/locks, and you add them as their own middleware with withLocks, next to withPersistence. Full guide: Locks.

ts
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'

const middleware = [
  withPersistence(memoryPersistence()),
  withLocks(new InMemoryLockStore()), // For many instances, use a distributed LockStore.
]