ai-persistence/build-cloudflare-adapter
Use when a Cloudflare Worker needs TanStack AI chat persistence — writes a chat-persistence.ts into the app against its D1 binding (raw or via Drizzle), plus a Durable Object LockStore. Covers per-request bindings, wrangler config, D1 migrations, and lease-based locks.
git clone --depth 1 https://github.com/TanStack/ai /tmp/ai-persistence-build-cloudflare-adapter && cp -r /tmp/ai-persistence-build-cloudflare-adapter/packages/ai-persistence/skills/ai-persistence/build-cloudflare-adapter ~/.claude/skills/ai-persistence-build-cloudflare-adapterSKILL.md
# Cloudflare Chat Persistence
The deliverable is **one file in the Worker** — `src/lib/chat-persistence.ts` —
exporting a factory that builds a `ChatPersistence` from the request's D1
binding, plus (when the app needs coordination) a Durable Object lock store.
Tables go into the app's existing `migrations/` directory and are applied with
`wrangler d1 migrations apply`.
Do not create a package or a migration runner. Wrangler already tracks applied
migrations; a second bookkeeping table only creates drift.
Read the **Store Reference**
(`docs/persistence/store-reference.md`) for the store contracts, and
**ai-persistence/stores** for the shape rules. This skill covers only
the Cloudflare-specific parts.
## 1. Read the app before writing anything
| Find | Where to look | What it decides |
| ---------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| D1 binding name | `wrangler.jsonc` `d1_databases[].binding` | `env.DB` vs `env.AI_STATE` in the factory |
| How `env` reaches code | the Worker `fetch(request, env)`, or an async-local helper (`getDb()`, `getCloudflareContext()`) | Whether the factory takes `env` or reads a helper |
| Drizzle or raw D1 | `drizzle-orm` in `package.json`, a `src/db/schema.ts` | Which recipe below to follow |
| Migrations dir | `wrangler.jsonc` `migrations_dir`, default `migrations/` | Where the new `.sql` file goes |
| Existing table names | the current migrations / schema | Prefix (`chat_*`) so nothing collides |
## 2. Two independent pieces
```
D1 database -> messages, runs, interrupts, metadata (AIPersistence.stores)
Durable Object -> LockStore (withLocks — NOT a store)
```
These do not compose into one object. `AIPersistence.stores` accepts exactly
four keys. Putting `locks` in the map throws
`Unknown AIPersistence store key: locks`; putting it in a `composePersistence`
override throws `Unknown AIPersistence override key: locks`. Both also fail to
type-check. Return
the state persistence from one factory and the lock store from another, then
wire them as two middlewares.
Most apps need only the first piece. Add the Durable Object when other
middleware genuinely needs mutual exclusion across isolates —
`InMemoryLockStore` gives none, because a Worker runs on many isolates at once.
## 3. Bindings are per-request
This is the one rule that separates Cloudflare from every other backend. A D1
binding does not exist at module scope, so `chat-persistence.ts` **must export a
factory**, not a const:
```ts ignore
import { defineAIPersistence } from '@tanstack/ai-persistence'
import type { ChatPersistence } from '@tanstack/ai-persistence'
/** Call inside a request handler — `env` is not available at module scope. */
export function chatPersistence(d1: D1Database): ChatPersistence {
return defineAIPersistence({
stores: {
messages: createMessageStore(d1),
runs: createRunStore(d1),
interrupts: createInterruptStore(d1),
metadata: createMetadataStore(d1),
},
})
}
```
Annotate `ChatPersistence` — bare `AIPersistence` is the all-optional bag and
`withPersistence` rejects it. Building it per request is cheap: the stores hold
no state beyond the binding.
## 4. The stores
Two routes, same invariants:
- **Drizzle over D1** — if the app already runs Drizzle, wrap the binding with
`drizzle(env.DB, { schema })` and follow
**ai-persistence/build-drizzle-adapter** verbatim (its "if `db` is
per-request" section is exactly this case). Stop reading here.
- **Raw D1** — implement the four stores against `d1.prepare(sql).bind(...)`:
`.first()` for `get`, `.all()` for `list*`, `.run()` for writes. D1 speaks
SQLite, so this mirrors the `node:sqlite` walkthrough in the guide one-for-one;
everything is already async, so no `Promise.resolve` wrapping.
The invariants are the whole game, whichever route you take:
| Store | Rule |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages` | `saveThread` is a full replace (`INSERT … ON CONFLICT(thread_id) DO UPDATE`) |
| `runs` | `createOrResume` reads first, else `INSERT … ON CONFLICT DO NOTHING`, then re-reads |
| `runs` | `update` on an unknown id is a silent no-op — never throws, never inserts |
| `runs` | `findActiveRun` (required) returns the latest `'running'` run for the thread, else null |
| `runs` | `listByThread` (optional) returns every run for the thread `ORDER BY started_at ASC` |
| `runs` | `listReclaimable` (optional) returns runs where `status = 'running' AND>
Triage all open GitHub issues, PRs, and discussions in the current repository by fanning out up to 100 parallel subagents (one per item), then produce a single prioritized report ranking which PRs to review first, which issues to address first, and which discussions need maintainer attention. Use when the user asks to "triage open issues/PRs", "triage discussions", "prioritize the backlog", "what should I review first", "sweep the repo", or any request to bulk-evaluate open GitHub work and recommend an order.
>
>
>
>
>
>