lukasa1993/tanstack-skills-extracted/tanstack-ai-persistence-build-cloudflare-adapter
Claude Code and agent skill by lukasa1993/tanstack-skills-extracted
- community
Checking that the SKILL.md file is still in the repository.
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.
Install
npx vskill@latest install lukasa1993/tanstack-skills-extracted/tanstack-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
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:
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 | listByParentRun (optional) returns child runs for one parent_run_id, ORDER BY started_at ASC. reconstructChat uses it to put subagent cards back | |
runs | listReclaimable (optional) returns runs where status = 'running' AND detached_since IS NOT NULL AND detached_since <= now - ttlMs (inclusive cutoff); it is a query, not automatic reclamation | |
interrupts | create is insert-if-absent; never clobber a resolved interrupt back to pending | |
interrupts | every list* ends ORDER BY requested_at ASC | |
metadata | reject nullish set with a clear TypeError; tell callers to use delete |
On runs, findActiveRun is required. listByThread, listByParentRun, and listReclaimable are optional, so implement those only if the app needs them. withPersistence calls createOrResume, update, and get. reconstruct.ts calls findActiveRun for rejoin-by-thread and listByParentRun to put subagent cards back. @tanstack/ai-sandbox's reapDetachedRuns calls listReclaimable. reconstruct.ts also calls listByThread to find the parent runs of children that a tool call started. Consumers of the optional methods feature-detect with store.method?.(...) and degrade when one is absent. The conformance testkit does not: each optional method you leave out must be listed in skipMethods or the suite fails. listByParentRun is the exception. When it is absent, the subagent checks skip on their own.
createOrResume copies parentRunId, subagentRunId, and name on the first insert. A later call for the same runId leaves them unchanged. If the caller omits a field, omit it on the mapped record.
RunRecord.error is a structured RunError ({ message: string, code?: string }), so the table gets two columns rather than one JSON blob: error for the provider's prose and error_code for the stable classification an operator filters and groups by. Write both together in update, so a later code-less failure can never leave a stale error_code from an earlier one behind, and omit code from the mapped record when the column is null: ...(row.error != null ? { error: { message: row.error, ...(row.error_code != null ? { code: row.error_code } : {}) } } : {}). Other row mappers omit absent optionals the same way (...(row.sandbox_key != null ? { sandboxKey: row.sandbox_key } : {})) so records compare cleanly against the reference in-memory backend. JSON columns are text: JSON.parse on read, JSON.stringify on write. Timestamps are integer epoch ms.
sandbox_key, detached_since, cancel_requested, and driver_epoch are the durable-agent-runs columns. In update, check 'field' in patch for all four — never patch.field !== undefined — because a reattach clears detachedSince by passing it explicitly as undefined, which must bind NULL into the SET clause rather than being filtered out of it (a filtered clear leaves the stale value and the run looks permanently detached to the reaper). The same applies to cancelRequested: false written explicitly is a real, meaningful value distinct from "never set", not something to coerce away. See examples/ts-react-chat/src/lib/sqlite-persistence.ts (D1 speaks the same SQLite dialect) for the worked update body.
5. The migration
Write the tables into the app's migrations/ directory as a new numbered file:
Apply with wrangler d1 migrations apply <database-name> (--local first, then --remote). If the app also uses Drizzle, generate this file with drizzle-kit generate instead of hand-writing it — the SQL and the Drizzle table definitions must agree, so let one of them own the other.
An existing chat_runs table does not get the three subagent columns from CREATE TABLE IF NOT EXISTS, and the chat_runs_parent_started index then fails. To add subagent support to an existing database, apply a separate migration first:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/tanstack-ai-persistence-build-cloudflare-adapter/SKILL.md
Popularity
- 7-day trend score
- 0
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/lukasa1993/tanstack-skills-extracted/tanstack-ai-persistence-build-cloudflare-adapter)