Skip to content

Commit c6cccfa

Browse files
committed
feat(persistence): add SQL migration CLI docs
1 parent 0271f9a commit c6cccfa

28 files changed

Lines changed: 813 additions & 490 deletions

docs/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@
407407
"label": "Overview",
408408
"to": "persistence/overview",
409409
"addedAt": "2026-06-18",
410-
"updatedAt": "2026-07-07"
410+
"updatedAt": "2026-07-08"
411411
},
412412
{
413413
"label": "Resumable Chat",
@@ -449,7 +449,7 @@
449449
"label": "Custom Stores",
450450
"to": "persistence/custom-stores",
451451
"addedAt": "2026-07-03",
452-
"updatedAt": "2026-07-06"
452+
"updatedAt": "2026-07-08"
453453
},
454454
{
455455
"label": "Sandbox Runs",

docs/persistence/cloudflare.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export function persistence(env: Env) {
3333
`d1` is required. `r2` and `durableObjects` are optional; include them only when
3434
your app needs blob/artifact storage or distributed locks.
3535

36+
You can also use R2 as the blob side of a hybrid persistence design while your
37+
app keeps runs, messages, events, metadata, and artifact indexes in a separate
38+
user-owned database. Implement that shape with [Custom Stores](./custom-stores):
39+
the Cloudflare backend is convenient when D1 owns the SQL state, but the R2
40+
artifact/blob pattern is not limited to D1.
41+
3642
For the end-to-end file persistence model, including generated artifacts,
3743
app-owned blobs, and sandbox workspace checkpoints backed by R2, see
3844
[Files and Artifacts](./files-and-artifacts).

docs/persistence/custom-stores.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ a packaged backend, start with [SQL Backends](./sql-backends) or
1212
[Cloudflare](./cloudflare). If you need the end-to-end chat journey, start with
1313
[Resumable Chat](./resumable-chat).
1414

15+
For production apps, this is the recommended ownership model: keep persistence
16+
inside your app's database, queue, object store, and retention policies, then
17+
expose those systems to TanStack AI through `AIPersistence` store callback
18+
methods. The packaged SQL and Cloudflare backends are TanStack primitives you
19+
can opt into when they match your stack, but the store contract is the stable
20+
boundary for user-owned persistence.
21+
1522
If you are deciding how to model generated files, workspace checkpoints, object
1623
bytes, metadata manifests, and locks together, read
1724
[Files and Artifacts](./files-and-artifacts) first. This page explains the store
@@ -65,6 +72,69 @@ const middleware = withPersistence(persistence, {
6572

6673
If any required store is missing, setup fails before the run starts.
6774

75+
`defineAIPersistence(...)` is only an identity helper for the aggregate store
76+
object. There is no separate high-level callback builder: implement the store
77+
methods directly and pass them under `stores`.
78+
79+
## Use your existing persistence boundary
80+
81+
Most production apps already have repositories or service methods for threads,
82+
runs, event logs, user decisions, metadata, and files. Wrap those methods in the
83+
store callbacks instead of adding a second persistence path.
84+
85+
```ts group=custom-stores
86+
import {
87+
defineAIPersistence,
88+
withPersistence,
89+
} from '@tanstack/ai-persistence'
90+
import type { MessageStore, RunStore } from '@tanstack/ai-persistence'
91+
import type { ModelMessage } from '@tanstack/ai'
92+
93+
type AppDb = {
94+
threads: {
95+
loadMessages: (threadId: string) => Promise<Array<ModelMessage>>
96+
replaceMessages: (
97+
threadId: string,
98+
messages: Array<ModelMessage>,
99+
) => Promise<void>
100+
}
101+
runs: {
102+
createOrResume: RunStore['createOrResume']
103+
update: RunStore['update']
104+
get: RunStore['get']
105+
}
106+
}
107+
108+
function appMessageStore(db: AppDb): MessageStore {
109+
return {
110+
loadThread: (threadId) => db.threads.loadMessages(threadId),
111+
saveThread: (threadId, messages) =>
112+
db.threads.replaceMessages(threadId, messages),
113+
}
114+
}
115+
116+
export function appPersistence(db: AppDb) {
117+
return defineAIPersistence({
118+
stores: {
119+
messages: appMessageStore(db),
120+
runs: db.runs,
121+
},
122+
})
123+
}
124+
125+
export function persistenceMiddleware(db: AppDb) {
126+
return withPersistence(appPersistence(db), {
127+
features: ['messages'],
128+
})
129+
}
130+
```
131+
132+
Add `publicEvents` when reconnect replay matters, `interrupts` when runs pause
133+
for user action, `internalEvents` for package or workflow checkpoints,
134+
`metadata` for app-owned correlation, `locks` for cross-process coordination,
135+
and `artifacts` plus `blobs` when runs produce durable files or media. Every
136+
persistence feature is supported by implementing the corresponding stores.
137+
68138
| Feature | Required stores |
69139
| --- | --- |
70140
| `messages` | `stores.messages` |
@@ -118,6 +188,12 @@ object storage, as the [Cloudflare backend](./cloudflare) does with D1 and R2.
118188
For concrete run-artifact, blob, and sandbox workspace examples, see
119189
[Files and Artifacts](./files-and-artifacts).
120190

191+
The same hybrid pattern works outside Workers: use your SQL database for runs,
192+
messages, events, metadata, and artifact indexes, then implement `stores.blobs`
193+
against R2 or another object store for large bytes. Keep the blob key or
194+
artifact id in your SQL-owned records so your app, not TanStack AI, controls
195+
garbage collection, access checks, and retention.
196+
121197
## Extend without growing the base schema
122198

123199
MCP and workflow packages should build on the common stores instead of adding

docs/persistence/files-and-artifacts.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ If you pass a manual `features` list to `withPersistence(...)`, include
2121
scenarios. Manual feature lists must pair `'artifacts'` with `'blobs'` because
2222
artifact metadata and stored bytes are enabled together.
2323

24+
The backend can be hybrid. A common production shape is a user-owned SQL
25+
database for runs, messages, events, metadata, and artifact indexes, plus R2 or
26+
another object store for large bytes behind `stores.blobs`. Use
27+
[Custom Stores](./custom-stores) when your app needs that split instead of a
28+
packaged backend.
29+
2430
For resumable image, audio, speech, transcription, and video hooks, start with
2531
[Resumable Generations](./resumable-generations). This page focuses on the
2632
artifact and blob records those endpoints create.

docs/persistence/overview.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ storage primitives with sandbox and workflow extensions.
1111
Persistence is optional middleware. A run with no persistence middleware behaves
1212
exactly as before.
1313

14+
## Control ladder
15+
16+
Persistence has several control levers. Start at the lowest rung that gives
17+
your app the durability it needs, then opt into more stores only when a feature
18+
requires them.
19+
20+
| Lever | You control | TanStack AI provides | Use it when |
21+
| --- | --- | --- | --- |
22+
| No persistence | Nothing | Normal in-memory run behavior | A run can disappear after the request ends. |
23+
| Client resume snapshot | Where the client stores `{ threadId, runId, cursor }` | `persistence.server`, `autoResume`, and `resume()` in the client hooks | A page refresh should reconnect to a still-durable server run. |
24+
| Messages | `stores.messages.loadThread` and `saveThread` | Server-authoritative thread history loading/saving | The server owns the transcript instead of trusting the browser. |
25+
| Durable replay | `stores.runs` and `stores.publicEvents` | Opaque cursors, run records, AG-UI event append, and replay | Reconnects must continue a run without calling the model again. |
26+
| Interrupts | `stores.interrupts` plus replay stores | Durable pending human decisions and resume payload handling | A run can pause for approval, input, or another user action. |
27+
| Internal checkpoints | `stores.internalEvents` | A separate non-UI event log | Packages or app workflows need checkpoints that should not replay to users. |
28+
| Metadata | `stores.metadata` | Namespaced key/value access | Your app needs durable correlation, manifests, or integration state. |
29+
| Locks | `stores.locks` | Shared lock capability consumed by integrations | Multiple workers can resume or mutate the same durable resource. |
30+
| Artifacts and blobs | `stores.artifacts` and `stores.blobs` | Durable artifact refs for generated files and media | Runs create downloadable files, media, or workspace checkpoints. |
31+
| SQL or platform backend | Database schema, migration timing, deployment topology | Backend factories that implement the same stores | You want TanStack primitives on top of your chosen database or platform. |
32+
33+
For production, prefer user-owned persistence through an `AIPersistence` object
34+
created with `defineAIPersistence(...)` or a backend factory that returns the
35+
same store contract. TanStack AI should call your existing storage boundary
36+
through store callback methods; it should not be the part of your production
37+
system that silently owns raw SQL schema lifecycle or data-retention policy.
38+
39+
Packaged backends such as SQLite, Postgres, Prisma, Drizzle, and Cloudflare are
40+
useful primitives when they fit your stack. They still expose the same
41+
`AIPersistence` stores, so you can start with a backend package, replace one
42+
store at a time, or compose SQL metadata with object storage for artifacts.
43+
1444
## What persistence stores
1545

1646
`withPersistence(...)` can:

docs/persistence/sql-backends.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ clients all expose the same `AIPersistence` stores to `withPersistence(...)`.
99
The shared SQL core includes a MySQL dialect for adapter authors, but the
1010
published raw backend packages are SQLite and Postgres.
1111

12+
For production, treat the SQL schema as part of your app-owned persistence
13+
boundary. Generate or copy the TanStack AI DDL into your normal migration
14+
system, review it like any other schema change, and deploy it before traffic
15+
uses the persistence stores. Use lazy migration only when you intentionally want
16+
the backend to create tables at runtime.
17+
1218
## Raw SQL backends
1319

1420
SQLite is file-backed and works well for local apps, prototypes, and single-node
15-
deployments.
21+
deployments. Fresh development examples can set `migrate: true` explicitly so a
22+
new local database creates its tables on first use.
1623

1724
```ts
1825
import { sqlitePersistence } from '@tanstack/ai-persistence-sqlite'
@@ -35,8 +42,8 @@ export const persistence = postgresPersistence({
3542
```
3643

3744
Both raw backends use the shared SQL core. They do not create persistence
38-
tables by default; set `migrate: true` to opt in to lazy table creation on
39-
first use.
45+
tables by default. `migrate` is `false` unless you pass `migrate: true`, which
46+
opts into lazy table creation on first use.
4047

4148
## Shared schema
4249

@@ -72,6 +79,33 @@ export const migrationStatements = ddl('postgres')
7279
Apply those statements with your normal migration system before traffic reaches
7380
the app.
7481

82+
## Generate SQL migrations
83+
84+
Use the generic SQL CLI when you want a migration file without importing DDL in
85+
application code. It emits the shared SQL persistence schema for every SQL
86+
dialect supported by the core generator: SQLite, Postgres, and MySQL.
87+
88+
```sh
89+
pnpm exec tanstack-ai-persistence-sql --dialect sqlite --out migrations/001_tanstack_ai.sql
90+
pnpm exec tanstack-ai-persistence-sql --dialect postgres --stdout
91+
pnpm exec tanstack-ai-persistence-sql --dialect mysql --out migrations/001_tanstack_ai.sql
92+
```
93+
94+
Options:
95+
96+
| Option | Purpose |
97+
| --- | --- |
98+
| `--dialect sqlite\|postgres\|mysql` | Choose the SQL dialect to generate. |
99+
| `--out <path>` | Write SQL to a migration file. |
100+
| `--stdout` | Print SQL instead of writing a file. |
101+
| `--timestamp <yyyymmddhhmmss>` | Use a deterministic timestamp in generated default names. |
102+
| `--name <name>` | Use a deterministic migration name. |
103+
| `--force` | Overwrite an existing output file. |
104+
105+
The generic CLI writes only the shared SQL persistence schema. Cloudflare R2
106+
artifact indexes, ORM-specific migration folder layouts, and app-owned tables
107+
remain separate.
108+
75109
## Drizzle and Prisma
76110

77111
Use the ORM-backed packages when your app already owns its database client and
@@ -100,6 +134,10 @@ pnpm exec tanstack-ai-persistence-prisma --dialect postgres
100134
pnpm exec tanstack-ai-persistence-drizzle --dialect postgres
101135
```
102136

137+
Use the ORM-specific CLIs when you want their default migration layout. Use the
138+
generic `tanstack-ai-persistence-sql` CLI when you only want SQL text or when
139+
you are generating MySQL DDL for a custom adapter.
140+
103141
The default Prisma path is
104142
`prisma/migrations/<timestamp>_tanstack_ai_persistence/migration.sql`. The
105143
default Drizzle path is `drizzle/<timestamp>_tanstack_ai_persistence.sql`.
@@ -144,6 +182,12 @@ import { ddl } from '@tanstack/ai-persistence-sql'
144182
export const migrationStatements = ddl('mysql')
145183
```
146184

185+
You can also generate the same MySQL DDL with the generic CLI:
186+
187+
```sh
188+
pnpm exec tanstack-ai-persistence-sql --dialect mysql --out migrations/tanstack_ai.sql
189+
```
190+
147191
## Choosing a backend
148192

149193
Use SQLite when one process owns the database file. Use Postgres when multiple

0 commit comments

Comments
 (0)