Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3e0e477
feat(persistence): add ai-persistence + drizzle/prisma/cloudflare bac…
AlemTuzlak Jul 22, 2026
29382a9
feat(ai-client): browser-refresh durability via the persistence option
AlemTuzlak Jul 22, 2026
0ed55b8
example(ts-react-chat): persistent chat with SQLite + reload durability
AlemTuzlak Jul 22, 2026
01bc50e
docs(persistence): add persistence section for server + browser durab…
AlemTuzlak Jul 22, 2026
2a92c33
docs(skills): cover persistence in middleware + chat-experience skills
AlemTuzlak Jul 22, 2026
c33dc5d
test(e2e): persistence browser-refresh durability spec
AlemTuzlak Jul 22, 2026
b443a9f
ci: apply automated fixes
autofix-ci[bot] Jul 22, 2026
89932a8
feat(ai-client): message-storage lever for the persistence option
AlemTuzlak Jul 23, 2026
e8aef35
docs(persistence): explain durability + persistence as one story
AlemTuzlak Jul 23, 2026
a14adc6
feat(ai-client): zero-config storage adapters + threadId-keyed persis…
AlemTuzlak Jul 23, 2026
a5ca7d2
feat(ai-persistence): reconstructChat server helper
AlemTuzlak Jul 23, 2026
ac68bf7
docs(persistence): recommended setup, zero-config adapters, header-ac…
AlemTuzlak Jul 23, 2026
e5c4064
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
8f408cb
docs(persistence): consolidate client persistence into one page
AlemTuzlak Jul 23, 2026
57aafee
docs(persistence): lead client-persistence with both use-cases
AlemTuzlak Jul 23, 2026
08d2ea3
fix(ai-client): keep host initialMessages in messages:false mode
AlemTuzlak Jul 23, 2026
cea0a83
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
caf0990
feat(ai-persistence)!: rename withChatPersistence to withPersistence
AlemTuzlak Jul 23, 2026
f2eeba6
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
462e15c
fix(persistence): reconcile with current @tanstack/ai core
AlemTuzlak Jul 23, 2026
2f2ab05
test(persistence): unskip two-phase approval->client-tool resume
AlemTuzlak Jul 23, 2026
15940bb
docs(example): ts-react-chat persistent-chat uses recommended server-…
AlemTuzlak Jul 23, 2026
978996e
feat(persistence-prisma): support Prisma 7 alongside Prisma 6
AlemTuzlak Jul 23, 2026
ad61ff5
fix(persistence): persist the terminal assistant reply on finish
AlemTuzlak Jul 23, 2026
c80ff23
docs(example): add server tools and a styled UI to persistent-chat
AlemTuzlak Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/client-browser-refresh-durability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@tanstack/ai-client': minor
'@tanstack/ai-react': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-angular': minor
'@tanstack/ai-preact': minor
---

Add browser-refresh durability to the `persistence` option.

The client `persistence` adapter now stores one combined record per chat id, the message transcript plus a resume snapshot, so a full page reload restores the conversation, rehydrates any pending interrupt, and rejoins a run that was still streaming (via `joinRun`, when the connection is durability-backed). A bare `UIMessage[]` from an older store is still read for backward compatibility.

The `persistence` option also accepts an object form, `{ store, messages?: boolean }`. `messages: false` caches only the tiny resume pointer (which run to rejoin, which interrupts are pending), keeping large transcripts off the client, durability rejoin and interrupt restore still work, and the server stays authoritative for history. A bare adapter is shorthand for `{ store, messages: true }`.

New web storage adapters are exported for this: `localStoragePersistence`, `sessionStoragePersistence`, and `indexedDBPersistence` (plus `StorageUnavailableError` and the `ChatPersistedState` / `ChatStorageAdapter` / `ChatPersistenceConfig` / `ChatPersistenceOption` types). Because durability rides the existing `persistence` option, every framework integration (`react`, `solid`, `vue`, `svelte`, `angular`, `preact`) gets it with no framework-specific code.
19 changes: 19 additions & 0 deletions .changeset/persistence-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai-persistence': minor
'@tanstack/ai-persistence-drizzle': minor
'@tanstack/ai-persistence-prisma': minor
'@tanstack/ai-persistence-cloudflare': minor
---

Add server-side persistence for `chat()`: durable thread messages, run records, and interrupts.

`withPersistence(persistence)` is a chat middleware that stores the conversation transcript, tracks each run's status, and records interrupt state so a paused run (tool approval, client-tool execution, generic interrupt) survives a server restart. Point it at any backend that implements the store contract:

- `@tanstack/ai-persistence` — the store contracts, the `withPersistence` / `withGenerationPersistence` middleware, and an in-memory reference store (`memoryPersistence`) plus a conformance testkit.
- `@tanstack/ai-persistence-drizzle` — Drizzle-backed stores (SQLite via `node:sqlite`, plus a bring-your-own-schema contract) with migration and schema CLIs.
- `@tanstack/ai-persistence-prisma` — Prisma-backed stores with a models CLI that emits a provider-neutral schema fragment. Works with both Prisma 6 (`prisma-client-js`) and Prisma 7 (`prisma-client`): the client argument is typed structurally, so it accepts a client generated to any output path.
- `@tanstack/ai-persistence-cloudflare` — D1-backed stores (delegating to the Drizzle backend) plus a Durable-Object lock store for cross-instance locking.

Resume reconstruction is delegated to the chat engine: persistence records interrupts and gates new input on a thread with pending interrupts, while the engine rebuilds the resume tool state from the resume batch and the interrupt bindings carried in the (server-loaded) message history.

`reconstructChat(persistence, request)` is a server helper that returns a thread's stored messages as a JSON `Response`, so a server-authoritative client can hydrate its transcript on load from a one-line `GET` handler.
153 changes: 0 additions & 153 deletions docs/chat/persistence.md

This file was deleted.

73 changes: 66 additions & 7 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,6 @@
"label": "Thinking & Reasoning",
"to": "chat/thinking-content",
"addedAt": "2026-04-15"
},
{
"label": "Persistence",
"to": "chat/persistence",
"addedAt": "2026-06-02",
"updatedAt": "2026-07-14"
}
]
},
Expand Down Expand Up @@ -224,7 +218,8 @@
{
"label": "Overview",
"to": "resumable-streams/overview",
"addedAt": "2026-07-17"
"addedAt": "2026-07-17",
"updatedAt": "2026-07-23"
},
{
"label": "Advanced",
Expand All @@ -238,6 +233,70 @@
}
]
},
{
"label": "Persistence",
"children": [
{
"label": "Overview",
"to": "persistence/overview",
"addedAt": "2026-07-22",
"updatedAt": "2026-07-23"
},
{
"label": "Chat Persistence",
"to": "persistence/chat-persistence",
"addedAt": "2026-07-22"
},
{
"label": "Client Persistence",
"to": "persistence/client-persistence",
"addedAt": "2026-07-22",
"updatedAt": "2026-07-23"
},
{
"label": "Controls",
"to": "persistence/controls",
"addedAt": "2026-07-22",
"updatedAt": "2026-07-23"
},
{
"label": "Custom Stores",
"to": "persistence/custom-stores",
"addedAt": "2026-07-22"
},
{
"label": "SQL Backends",
"to": "persistence/sql-backends",
"addedAt": "2026-07-22"
},
{
"label": "Drizzle",
"to": "persistence/drizzle",
"addedAt": "2026-07-22"
},
{
"label": "Prisma",
"to": "persistence/prisma",
"addedAt": "2026-07-22",
"updatedAt": "2026-07-23"
},
{
"label": "Cloudflare",
"to": "persistence/cloudflare",
"addedAt": "2026-07-22"
},
{
"label": "Migrations",
"to": "persistence/migrations",
"addedAt": "2026-07-22"
},
{
"label": "Internals",
"to": "persistence/internals",
"addedAt": "2026-07-22"
}
]
},
{
"label": "Protocol",
"children": [
Expand Down
89 changes: 89 additions & 0 deletions docs/persistence/chat-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Chat Persistence
id: chat-persistence
---

# Chat Persistence

You want a conversation to outlive a single request: the transcript, whether
each run finished or is still waiting on an interrupt, all still there after the
process restarts. `withPersistence` is a chat middleware that writes that
state to a store you choose, so the server owns an authoritative copy of every
thread.

## Persist state on the server

Add the middleware to `chat()` and point it at a backend. Here it is a local
SQLite file via the Drizzle backend:

```ts
import {
chat,
chatParamsFromRequestBody,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { withPersistence } from '@tanstack/ai-persistence'
import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite'

// One store for the whole process. `migrate: true` applies the bundled schema.
const persistence = sqlitePersistence({
url: 'file:.data/chat.sqlite',
migrate: true,
})

export async function POST(request: Request) {
const params = await chatParamsFromRequestBody(await request.json())
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
// Forward the resume batch so a thread with pending interrupts continues.
...(params.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream)
}
```

The middleware uses whichever stores the backend provides, no feature flags:

- `messages` loads and saves the full model-message thread.
- `runs` records running, completed, failed, or interrupted status.
- `interrupts` records pending tool-approval / client-tool / generic waits, and
requires `runs`.
- `locks` is handed to other middleware for cross-worker coordination.

`migrate: true` is convenient for local development. In production, apply the
bundled migrations through your deployment workflow instead. See
[Migrations](./migrations).

## Send the full transcript, or none of it

`withPersistence` follows one rule, the authoritative-history contract:

- A request with a **non-empty** `messages` array is the full conversation. On
finish it **overwrites** the stored thread. Post the complete transcript, not
a delta, or you replace the stored thread with just the newest message.
- A request with an **empty** `messages` array continues a stored thread. The
middleware loads the stored transcript and the run picks up from there, so the
client does not have to re-send history.

## Interrupts survive a restart

When a run pauses on an interrupt (a tool approval, a client-side tool, a
generic wait), the middleware records it. A later request on that thread must
carry a `resume` batch that answers the pending interrupts before new input is
accepted, otherwise it is rejected, which is why the example above forwards
`params.resume`. The chat engine itself rebuilds the resume state from that
batch and the interrupt bindings in the loaded history, so the persistence layer
only records the interrupts and gates the thread.

## Where to go next

- Bring durability to the browser too, so a full page reload restores the
conversation and rejoins an in-flight run: [Client persistence](./client-persistence).
- Pick a backend: [Drizzle](./drizzle), [Prisma](./prisma),
[Cloudflare](./cloudflare), or [your own store](./custom-stores).
- Choose which stores to run: [Controls](./controls).
Loading