ai-persistence/build-custom-adapter

Implements a custom TanStack AI chat persistence adapter for databases without dedicated recipes.

3.1k|316|Updated Oct 8, 2025
One-click install
npx skills add https://github.com/TanStack/ai --skill ai-persistence-build-custom-adapter
Or copy as Structured Prompt for Agent
Please help me install this Agent Skill.
Skill: ai-persistence/build-custom-adapter
Source: https://github.com/TanStack/ai/tree/main/packages/ai-persistence/skills/ai-persistence/build-custom-adapter
Command: npx skills add https://github.com/TanStack/ai --skill ai-persistence-build-custom-adapter

SYSTEM DOCUMENTATION & REQUIREMENTS

💡 This Skill requires @tanstack/ai-persistence, @tanstack/ai.

What problem does it solve?

TanStack AI chat persistence needs four stores (messages, runs, interrupts, metadata) with strict idempotency invariants, but databases like raw Postgres, Kysely, node:sqlite, MongoDB, or Redis have no dedicated adapter recipe. This Skill writes a working chat-persistence.ts against the app's existing database client without inventing new infrastructure.

Core Features & Use Cases

  • Custom adapter generation: Produces a single src/lib/chat-persistence.ts exporting a ChatPersistence built from the app's existing client, with DDL added through the app's own migration flow.
  • Invariant enforcement: Encodes the seven idempotency invariants (full-overwrite saveThread, insert-if-absent createOrResume, silent no-op updates, explicit-clear vs omitted-field handling) that prevent stuck approvals and wiped history.
  • Engine-specific guidance: Covers Postgres/pg, Kysely, node:sqlite, MongoDB, and Redis with driver-specific patterns for JSON handling, composite keys, and upsert semantics.
  • Use Case: An app using raw pg with no ORM needs durable chat threads and resumable runs; this Skill writes the four stores, wires withPersistence into the chat route, and sets up the conformance test suite.

Quick Start

Ask the AI to build a TanStack AI chat persistence adapter for your existing database client, for example: "Write chat persistence for my app using the pg pool in src/db.ts and verify it with the conformance testkit."

Frequently Asked Questions about ai-persistence/build-custom-adapter

High-intent search queries and answers about installing and using this skill.

FAQPage Schema
How do I build a custom TanStack AI chat persistence adapter?

Create a single chat-persistence.ts that implements the four stores (messages, runs, interrupts, metadata) against your existing database client, then assemble them with defineAIPersistence. Follow the seven idempotency invariants and verify with runPersistenceConformance from the testkit.

When should I use a custom adapter instead of the Drizzle or Prisma skills?

Use a custom adapter only when your database has no dedicated recipe, such as raw pg, Kysely, node:sqlite, MongoDB, or Redis. If the app already runs Drizzle, Prisma, or Cloudflare D1, route to those skills since they contain driver-specific code.

Can I use Redis for TanStack AI chat persistence?

Redis works well for metadata and locks but is awkward for interrupts because listings need ordered secondary indexes you maintain by hand. A common split is Postgres for messages, runs, and interrupts with Redis for locks, combined via composePersistence.

Why do resumed runs get stuck or lose their detached state?

The usual cause is checking patch.field !== undefined instead of 'field' in patch in runs.update, which cannot distinguish an omitted field from an explicit clear. An explicit undefined for detachedSince must write NULL, or reattached runs look permanently detached to the reaper.

How do I verify a custom persistence adapter is correct?

Run runPersistenceConformance from @tanstack/ai-persistence/testkit against a throwaway database, declaring intentional omissions with skip and unimplemented optional methods with skipMethods. The suite checks all stores and the idempotency invariants.

How should composite metadata keys be stored in MongoDB?

Use a compound _id subdocument or a unique index on { namespace, key }, never a delimiter-joined string. Joined keys collide because ('a:b','c') and ('a','b:c') must remain distinct records, which the conformance suite checks.