Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .changeset/persistence-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Add server-side persistence for `chat()`: durable thread messages, run records,
`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-drizzle` β€” Drizzle-backed stores for SQLite and Postgres behind one `drizzlePersistence(db, { provider, schema })` entry (plus a `node:sqlite` convenience factory), sharing one bring-your-own-schema contract. Get the tables into your drizzle-kit journal by re-exporting the `/sqlite-schema` or `/pg-schema` subpath (stock tables, tracks upgrades) or by emitting an owned starter with the dialect-aware schema CLI (renames, extra columns, index tuning).
- `@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.

Expand Down
12 changes: 8 additions & 4 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,14 @@
{
"label": "SQL Backends",
"to": "persistence/sql-backends",
"addedAt": "2026-07-22"
"addedAt": "2026-07-22",
"updatedAt": "2026-07-24"
},
{
"label": "Drizzle",
"to": "persistence/drizzle",
"addedAt": "2026-07-22"
"addedAt": "2026-07-22",
"updatedAt": "2026-07-24"
},
{
"label": "Prisma",
Expand All @@ -291,12 +293,14 @@
{
"label": "Migrations",
"to": "persistence/migrations",
"addedAt": "2026-07-22"
"addedAt": "2026-07-22",
"updatedAt": "2026-07-24"
},
{
"label": "Internals",
"to": "persistence/internals",
"addedAt": "2026-07-22"
"addedAt": "2026-07-22",
"updatedAt": "2026-07-24"
}
]
},
Expand Down
9 changes: 4 additions & 5 deletions docs/persistence/chat-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ 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.
// One store for the whole process. Stock defaults + runtime table bootstrap.
const persistence = sqlitePersistence({
url: 'file:.data/chat.sqlite',
migrate: true,
})

export async function POST(request: Request) {
Expand All @@ -55,9 +54,9 @@ The middleware uses whichever stores the backend provides, no feature flags:
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).
The `/sqlite` factory bootstraps stock tables for local development. In
production, emit a schema with `tanstack-ai-drizzle-schema` and migrate via
your own drizzle-kit journal. See [Migrations](./migrations).

## Send the full transcript, or none of it

Expand Down
3 changes: 2 additions & 1 deletion docs/persistence/client-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ loader (or equivalent). See [Chat persistence](./chat-persistence).

```ts
import { reconstructChat } from '@tanstack/ai-persistence'
import { persistence } from './persistence'
import type { Scope } from '@tanstack/ai'

export async function GET(request: Request) {
Expand Down Expand Up @@ -162,7 +163,7 @@ and hand it to `useChat` as `initialResumeSnapshot`. A bare in-flight snapshot i
rejoined just like a persisted pointer; a client that started the run still rejoins
via its own pointer, so pass this only when the running client is a different one.

```tsx
```tsx ignore
// Server: the loader (or your GET endpoint) reports the active run alongside history.
loader: async () => ({
messages: await loadHistory(threadId), // reconstructChat, etc.
Expand Down
262 changes: 163 additions & 99 deletions docs/persistence/drizzle.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,194 @@ id: drizzle

# Persistence with Drizzle

`@tanstack/ai-persistence-drizzle` supports SQLite-family Drizzle databases.
It has two entry points:
`@tanstack/ai-persistence-drizzle` is **schema-first**. It does not ship SQL
migrations. You own the schema file (or accept stock defaults), generate DDL
with **your** drizzle-kit journal, and pass the schema into the runtime.

- the package root accepts an already-created, migrated `DrizzleSqliteDb` and is
Two entry points:

- the package root accepts an already-created, migrated Drizzle database plus
a required `provider` (`'sqlite'` or `'pg'`) and matching `schema`, and is
safe to import in edge runtimes;
- `/sqlite` is a Node-only convenience factory built on `node:sqlite`.
- `/sqlite` is a Node-only convenience factory built on `node:sqlite` with stock
defaults and optional runtime table bootstrap for local/dev.

The `provider` discriminates the whole call: with `provider: 'sqlite'` the
compiler only accepts a SQLite Drizzle database and a SQLite schema, with
`provider: 'pg'` only a Postgres database and schema, and the runtime assert
verifies the passed tables really are that dialect.

## Schema first

There are two ways to get the tables into **your** drizzle-kit journal:

### Stock tables: re-export the package schema

If you don't need to rename or extend the tables, don't copy anything β€” create
a one-line module the package keeps up to date across upgrades:

```ts
// src/db/tanstack-ai-schema.ts
export * from '@tanstack/ai-persistence-drizzle/sqlite-schema'
```

Add that file to your drizzle-kit `schema` paths and generate migrations as
usual. When a package upgrade adds a column or index, `drizzle-kit generate`
picks it up automatically β€” there is no copied file to go stale. Use
`@tanstack/ai-persistence-drizzle/pg-schema` for Postgres.

### Custom tables: emit an owned starter

To rename tables or columns, add app-owned columns, or tune indexes, own the
definition instead. Emit a starter schema into your project:

```bash
pnpm exec tanstack-ai-drizzle-schema --out src/db
```

Add it to drizzle-kit so **your** journal owns the DDL:

```ts ignore
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
dialect: 'sqlite',
schema: ['./src/db/schema.ts', './src/db/tanstack-ai-schema.ts'],
out: './drizzle',
})
```

```bash
pnpm exec drizzle-kit generate
pnpm exec drizzle-kit migrate
```

Then wire the runtime:

```ts
import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle'
import { schema } from './db/tanstack-ai-schema'
import { db } from './db'

export const persistence = drizzlePersistence(db, {
provider: 'sqlite',
schema,
})
```

Because the runtime operates on the table objects you pass, the file is yours
to shape:

- **Rename tables and columns**, or drop the explicit column names and rely on
your drizzle `casing` configuration β€” the stores read database names from
your objects, so the generated SQL follows your conventions.
- **Add app-owned columns** β€” for example a `userId` column on `messages` to
scope threads to users. Keep added columns nullable or defaulted so the
store inserts succeed; the TanStack AI stores never read or write them.
- **Tune indexes** β€” the starter ships lookup indexes on
`interrupts.thread_id` and `interrupts.run_id` (the columns the stores list
by); add composite or partial indexes as your query patterns demand.
- **Keep the contract columns** with their data shapes. The
`TanstackAiSqliteSchema` type enforces the shapes at compile time, and
`drizzlePersistence` validates the tables and columns exist at construction.

## Node SQLite convenience

For local development without a project schema file yet:

```ts
import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite'

There is no dialect option. For MySQL, PostgreSQL, or another Drizzle dialect,
implement the `AIPersistence` stores for that database or use the Prisma
backend.
export const persistence = sqlitePersistence({
url: 'file:.tanstack-ai/state.sqlite',
})
```

## Node SQLite
This uses `createDefaultSqliteSchema()` and, by default, creates missing tables
with `CREATE TABLE IF NOT EXISTS` derived from that schema. That is a **bootstrap
convenience**, not a migration system β€” for production, emit the schema, migrate
with drizzle-kit, and pass your schema with `ensureTables: false`:

```ts
import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite'
import { schema } from './db/tanstack-ai-schema'

export const persistence = sqlitePersistence({
url: 'file:.tanstack-ai/state.sqlite',
migrate: true,
schema,
ensureTables: false,
})
```

`url` may be `:memory:`, a filesystem path, or a `file:`-prefixed path.
`migrate: true` applies the bundled migrations before creating stores. Prefer
deployment-time migrations in production.

## Bring your own SQLite Drizzle database

```ts ignore
import { drizzle } from 'drizzle-orm/d1'
import { drizzlePersistence, schema } from '@tanstack/ai-persistence-drizzle'
import {
createDefaultSqliteSchema,
drizzlePersistence,
} from '@tanstack/ai-persistence-drizzle'

export function createPersistence(state: D1Database) {
const schema = createDefaultSqliteSchema()
const db = drizzle(state, { schema })
return drizzlePersistence(db)
return drizzlePersistence(db, { provider: 'sqlite', schema })
}
```

The root entry does not import Node built-ins and works with Cloudflare D1 and
other SQLite-compatible Drizzle drivers. The application owns connection
lifecycle and migration timing.
Prefer emitting and owning the schema in production. The root entry does not
import Node built-ins and works with Cloudflare D1 and other SQLite-compatible
Drizzle drivers. The application owns connection lifecycle and migration timing.

## Postgres

Postgres uses the same root entry with `provider: 'pg'`: bring your own
migrated Drizzle Postgres database (node-postgres, postgres.js, Neon,
PGlite, …) and your schema. For stock tables, re-export
`@tanstack/ai-persistence-drizzle/pg-schema` as shown above; to own the
definition, emit the Postgres starter:

```bash
pnpm exec tanstack-ai-drizzle-schema --out src/db --dialect pg
```

Add it to your drizzle-kit config (`dialect: 'postgresql'`), generate and run
migrations, then wire the runtime:

```ts ignore
import { drizzle } from 'drizzle-orm/node-postgres'
import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle'
import { schema } from './db/tanstack-ai-schema'

const db = drizzle(process.env.DATABASE_URL!)
export const persistence = drizzlePersistence(db, { provider: 'pg', schema })
```

The same schema freedoms apply: rename tables and columns, lean on drizzle
`casing`, and add nullable or defaulted app-owned columns. The
`TanstackAiPgSchema` type enforces the contract's column data shapes at compile
time. For local development without migrations, `createDefaultPgSchema()`
provides the stock tables and `ensurePgTables` bootstraps them with
`CREATE TABLE IF NOT EXISTS`:

```ts ignore
import { drizzle } from 'drizzle-orm/node-postgres'
import { pool } from './db'
import {
createDefaultPgSchema,
drizzlePersistence,
ensurePgTables,
} from '@tanstack/ai-persistence-drizzle'

const schema = createDefaultPgSchema()
await ensurePgTables((sql) => pool.query(sql), schema)
export const persistence = drizzlePersistence(drizzle(pool), {
provider: 'pg',
schema,
})
```

## Use the middleware

Expand Down Expand Up @@ -79,86 +226,3 @@ no distributed lock, so consumers that need one fall back to an in-process
lock. When multiple workers must share a lock service, use `composePersistence`
to add a distributed `locks` implementation β€” for example the Cloudflare
Durable Object lock from `@tanstack/ai-persistence-cloudflare`.

## Get the migrations

The package exports the ordered `sqliteMigrations` manifest:

```ts
import { sqliteMigrations } from '@tanstack/ai-persistence-drizzle'

for (const migration of sqliteMigrations) {
console.log(migration.id, migration.filename)
}
```

Or copy canonical SQL files with the CLI:

```bash
pnpm exec tanstack-ai-drizzle-migrations --out migrations/tanstack-ai
```

Use `--stdout` to print the SQL. The CLI refuses to replace a divergent file
unless `--force` is passed. Commit the copied files and apply them using your
normal SQLite, D1, or Drizzle deployment workflow.

## Schema ownership

The exported `schema` contains `messages`, `runs`, `interrupts`, and
`metadata`. Application tables may live beside these tables in the same
database.

## Own the schema

If your project already uses drizzle-kit, you can own the TanStack AI schema
outright instead of applying the bundled SQL. Emit the schema module into your
project:

```bash
pnpm exec tanstack-ai-drizzle-schema --out src/db
```

This writes `src/db/tanstack-ai-schema.ts` β€” a regular Drizzle schema file that
imports from **your** installed `drizzle-orm`. The CLI refuses to replace a
divergent file unless `--force` is passed; `--stdout` prints the module instead.

Add the file to your drizzle-kit schema paths so your own migration journal
owns the DDL:

```ts ignore
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
dialect: 'sqlite',
schema: ['./src/db/schema.ts', './src/db/tanstack-ai-schema.ts'],
out: './drizzle',
})
```

Then pass the schema back so the runtime reads and writes through your copy:

```ts
import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle'
import { schema } from './tanstack-ai-schema'
import { db } from './db'

export const persistence = drizzlePersistence(db, { schema })
```

Because the runtime operates on the table objects you pass, the file is truly
yours to shape:

- **Rename tables and columns**, or drop the explicit column names and rely on
your drizzle `casing` configuration β€” the stores read database names from
your objects, so the generated SQL follows your conventions.
- **Add app-owned columns** β€” for example a `userId` column on `messages` to
scope threads to users. Keep added columns nullable or defaulted so the
store inserts succeed; the TanStack AI stores never read or write them.
- **Keep the contract columns** with their data shapes. The
`TanstackAiSqliteSchema` type enforces the shapes at compile time, and
`drizzlePersistence` validates the tables and columns exist at construction.

When you own the schema this way, migrations flow entirely through your
drizzle-kit journal β€” package upgrades that change the schema surface as
drizzle-kit diffs when you update the emitted file. Don't mix this with the
bundled SQL migrations: pick one DDL owner per database.
5 changes: 3 additions & 2 deletions docs/persistence/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ explicitly widened types can bypass static guarantees.

Packaged backends own resources differently:

- Drizzle accepts a migrated SQLite-family database; its root import is
edge-safe. The `/sqlite` entry creates a Node SQLite connection.
- Drizzle is schema-first: pass a required `schema` (emit via CLI; your
drizzle-kit owns migrations). The root import is edge-safe. The `/sqlite`
entry creates a Node SQLite connection with stock defaults.
- Prisma accepts the application's generated and migrated client.
- Cloudflare maps D1 to structured stores and Durable Objects to locks.

Expand Down
Loading
Loading