lukasa1993/tanstack-skills-extracted/tanstack-ai-core-client-persistence
Claude Code and agent skill by lukasa1993/tanstack-skills-extracted
- community
Checking that the SKILL.md file is still in the repository.
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. Also covers generation hooks (useGenerateImage etc.), which take only the server-driven mode: persistence: true hydrates the last generation for the (REQUIRED) threadId from the server on mount and repaints status/result/error, nothing is cached in the browser. No extra package: the adapters ship in the framework packages.
Install
npx vskill@latest install lukasa1993/tanstack-skills-extracted/tanstack-ai-core-client-persistenceSKILL.md
Client Persistence
Builds on ai-core, and onai-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 alreadyhas. 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):
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)
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)
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 can joinRun and 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.
Generation hooks: server-driven only
The generation hooks (useGenerateImage, useGenerateVideo, useGeneration, useSummarize, useTranscription, …) take a persistence option too, but it is boolean only — there is no storage-adapter mode, and the browser caches nothing. The hooks are transparent, mirroring `useChat`: a reload repaints the hook's normal fields — status ('idle' / 'generating' / 'success' / 'error'), error, and result — as if the run had just finished. There is no resumeSnapshot, resumeState, pendingArtifacts, or resultArtifacts field. The one extra field is runId: the id of the generation job currently running, or null when nothing is in flight. The persisted record holds run identity, status, error, and result metadata (ids, model, a provider video job id), never the generated media bytes.
The hook return is exactly generate / result / isLoading / error / status / stop / reset / runId.
Turning it on (persistence: true)
The server half — the same route handles the run and the hydration GET:
- Nothing is cached client-side. On mount the client hydrates the **last
generation** for its threadId from the server via the connection's hydrateGeneration handler (the SSE/HTTP adapters issue a GET with ?threadId= to the same endpoint URL) and repaints it into the normal fields.
- The server
GETreturnsreconstructGeneration(persistence, request)from
@tanstack/ai-persistence — it resolves the run by ?runId= (preferred) or the latest run linked to ?threadId=, and needs stores.generationRuns. Pair it with withGenerationPersistence on the generation route. See ai-core/media-generation and ai-persistence.
- Best for multi-device / compliance (no generation metadata in browser
storage), exactly like chat's server-authoritative mode.
Restoring media: byte storage + artifactUrl
result comes back with its media only when the server persists the bytes (stores.artifacts + stores.blobs) AND withGenerationPersistence is given an artifactUrl mapper:
artifactUrl stamps a durable app-origin URL onto each persisted ref and rewrites the live result's media to it, so live and restored results match. The durable refs travel on result.artifacts; on restore the hook rebuilds result from them, so result.images[i].url (or a video's result.url) serves from your own origin. result.artifacts is the whole artifact surface on the hook. Without byte storage, a reload restores status / error and result stays null.
Also worth knowing:
stop()marks the record no longer resumable;reset()clears the in-memory
snapshot.
- Nothing auto-runs from a hydrated record —
generate(...)is always explicit. - Use
status/resultfor a finished run; userunIdto tell that a run was
still generating when the page closed, and to name it to your own server (to cancel or poll the provider job — stop() only aborts the local stream).
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
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-core-client-persistence/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-core-client-persistence)