
Skill
ai-core/client-persistence
implement browser chat persistence for TanStack AI
Description
Browser chat persistence on useChat / ChatClient: localStoragePersistence, sessionStoragePersistence, indexedDBPersistence. Client-authoritative (adapter, full transcript) vs server-authoritative (persistence: true, no client cache). Reload restore, pending interrupts, mid-stream rejoin with delivery durability. Use for SPA reload durability — NOT server history alone. No extra package: the adapters ship in the framework packages.
SKILL.md
Client Persistence
Builds on ai-core, and on
ai-core/chat-experienceforuseChatitself.No extra package. The adapters below ship in the framework packages (
@tanstack/ai-reactand friends, re-exported from@tanstack/ai-client), so browser persistence needs nothing installed beyond what a chat UI already has. The server half is a separate package — see@tanstack/ai-persistenceand itsai-persistence/serverskill.
A ChatClient / useChat keeps messages in memory. The persistence option
stores one record per threadId so a reload can repaint the transcript,
restore a pending interrupt, and rejoin an in-flight run.
Import adapters from the framework package (not @tanstack/ai-client
unless vanilla JS):
import {
useChat,
fetchServerSentEvents,
localStoragePersistence,
sessionStoragePersistence,
indexedDBPersistence,
} from '@tanstack/ai-react'
Adapters
| Adapter | Survives | Notes |
|---|---|---|
localStoragePersistence() | Reloads + browser restarts | Sync hydrate; quota-bound; JSON codec default |
sessionStoragePersistence() | Reloads in the same tab | Cleared when tab/session ends |
indexedDBPersistence() | Reloads + restarts | Async open (first paint may be empty briefly); structured clone |
All default to the chat persisted-state shape — no type argument or codec required for normal use.
Mode A — cache everything (client-authoritative)
function Chat() {
const { messages, sendMessage } = useChat({
threadId: 'support-chat', // stable — required
connection: fetchServerSentEvents('/api/chat'),
persistence: localStoragePersistence(),
})
// ...
}
Bare adapter ≡ full transcript + resume pointer. Browser owns history; server
(if any) mirrors when you post non-empty messages.
Best for: SPA, offline-first, single device, moderate conversation size.
Mode B — server-authoritative (persistence: true)
function Chat({ threadId }: { threadId: string }) {
const { messages, sendMessage } = useChat({
threadId,
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
})
// ...
}
Nothing is cached client-side: no transcript, no resume pointer.
On mount, useChat hydrates the thread from the server by threadId
(paint + tail active run). Same path for another device. Pair with server
withPersistence + a hydrate route (reconstructChat or equivalent).
Best for: large transcripts, multi-device, compliance (no message bodies in browser storage).
What a reload restores
- Finished run — transcript from the adapter (mode A) or server (mode B).
- Paused on interrupt — approval UI restored (from the adapter in mode A, the server hydrate in mode B).
- Still streaming — needs delivery durability on the route
(
toServerSentEventsResponse(stream, { durability: … })) so the client canjoinRunand finish the reply. Persistence alone is not enough.
Stable threadId is the identity
Persistence keys on threadId. The hooks have no separate id option — a
chat's identity is its threadId. Without a stable one, each load is a new
chat. Generate it server-side or from a route param the user owns; do not
randomize per mount.
Common mistakes
HIGH: No threadId
Record cannot be found after reload.
HIGH: Passing id to useChat
Removed — threadId is the identity. (ChatClient still accepts id directly
as a lower-level escape hatch for keying storage separately from the wire
thread; the framework hooks do not.)
HIGH: persistence: true without server history
Empty chat after reload unless the server can reconstruct by threadId.
MEDIUM: Huge transcripts in localStorage
Quota and main-thread cost. Prefer persistence: true + server store, or
IndexedDB with care.
MEDIUM: Expecting multi-device sync from client storage alone
localStorage is per-browser. Use server persistence for multi-device.
Cross-references
- ai-persistence/server (
@tanstack/ai-persistence) — authoritative server half - ai-core/chat-experience —
useChat, resumable connections - Resumable streams docs — mid-stream rejoin
More skills from the ai repository
View all 27 skillsai-code-mode
execute sandboxed TypeScript code with LLMs
Jul 16AI SDKCode ExecutionSandboxingTanStack +1ai-core
configure TanStack AI agent features
Jul 30AgentsAIMiddlewareai-core/adapter-configuration
configure AI provider adapters
Jul 16AI SDKConfigurationLLMTanStackai-core/ag-ui-protocol
implement TanStack AI streaming protocol
Jul 16API DevelopmentLLMTanStackai-core/chat-experience
implement chat experiences with TanStack AI
Jul 30API DevelopmentFrontendLLMTanStackai-core/custom-backend-integration
integrate custom backends with TanStack AI
Jul 17AIAPI DevelopmentBackendTanStack
More from TanStack
View publisheraggregation
perform data aggregation in TanStack Table
table
Jul 30Data AnalysisFrontendTanStackapi-not-found
diagnose TanStack Table API errors
table
Jul 30DebuggingFrontendTanStackcell-selection
select rectangular cell ranges in tables
table
Jul 30Data AnalysisTanStackUI Componentsclient-vs-server
manage TanStack Table data pipelines
table
Jul 30Data PipelineFrontendPerformanceTanStackcolumn-faceting
build faceted filter UIs
table
Jul 30Data VisualizationFrontendTanStackcolumn-filtering
implement column filtering in TanStack Table
table
Jul 30Data AnalysisFrontendTanStack