From 702dca1965d625279aa2c44c2771036dcd349090 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Thu, 4 Jun 2026 17:22:26 +0300 Subject: [PATCH 1/4] feat: add TanStack AI chat persistence tutorial using Upstash Redis --- docs.json | 1 + redis/tutorials/tanstack_chat_persistence.mdx | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 redis/tutorials/tanstack_chat_persistence.mdx diff --git a/docs.json b/docs.json index c98014404..3f674ff86 100644 --- a/docs.json +++ b/docs.json @@ -868,6 +868,7 @@ "redis/tutorials/python_url_shortener", "redis/tutorials/api_with_cdk", "redis/tutorials/agent_memory", + "redis/tutorials/tanstack_chat_persistence", "redis/tutorials/analytics_with_redis", "redis/tutorials/auto_complete_with_serverless_redis", "redis/tutorials/aws_app_runner_with_redis", diff --git a/redis/tutorials/tanstack_chat_persistence.mdx b/redis/tutorials/tanstack_chat_persistence.mdx new file mode 100644 index 000000000..ad9a4a7d6 --- /dev/null +++ b/redis/tutorials/tanstack_chat_persistence.mdx @@ -0,0 +1,121 @@ +--- +title: TanStack AI Chat Persistance +description: Use Upstash Redis to persist TanStack AI chat histories across reloads, navigation, and devices with a simple adapter. +--- + +By default a TanStack AI `ChatClient` keeps messages in memory only, so they vanish on reload. TanStack AI exposes a tiny persistence interface - `getItem` / `setItem` / `removeItem` - and any backend that implements it becomes durable storage. + +Upstash Redis is a great fit: it's serverless with a REST API (no connection pooling, works in any edge/serverless runtime), latency is low enough to write on every streamed token, and per-conversation keys with an optional TTL give you free expiry of stale chats. + +This tutorial uses OpenAI for the model, but persistence is model-agnostic. + +## Prerequisites + +- An [Upstash Redis](https://console.upstash.com) database +- A TanStack AI `ChatClient` (`@tanstack/ai-client`) +- `@upstash/redis` + +```bash +npm install @tanstack/ai-client @upstash/redis +``` + +```bash +UPSTASH_REDIS_REST_URL="https://..." +UPSTASH_REDIS_REST_TOKEN="..." +``` + +## The adapter + +A persistence adapter is just an object with three methods. Each may be sync or async — the client awaits them. We store the messages array under a namespaced key and revive `createdAt` (which becomes a string through JSON) on read. + +```typescript +// upstash-persistence.ts +import type { Redis } from "@upstash/redis"; +import type { ChatClientPersistence, UIMessage } from "@tanstack/ai-client"; + +export function upstashPersistence( + redis: Redis, + { prefix = "tanstack:chat:", ttlSeconds }: { prefix?: string; ttlSeconds?: number } = {}, +): ChatClientPersistence { + const key = (id: string) => `${prefix}${id}`; + + return { + async getItem(id) { + const stored = await redis.get>(key(id)); + if (!stored) return null; + // createdAt round-trips as a string through JSON; revive it. + return stored.map((m) => ({ + ...m, + createdAt: typeof m.createdAt === "string" ? new Date(m.createdAt) : m.createdAt, + })); + }, + async setItem(id, messages) { + await redis.set(key(id), messages, ttlSeconds ? { ex: ttlSeconds } : undefined); + }, + async removeItem(id) { + await redis.del(key(id)); + }, + }; +} +``` + +## Use it + +Pass the adapter as `persistence` and give the client a stable `id` — that `id` is the storage key, so the same `id` loads the same conversation back. + +```typescript +import { Redis } from "@upstash/redis"; +import { ChatClient } from "@tanstack/ai-client"; +import { upstashPersistence } from "./upstash-persistence"; + +const redis = Redis.fromEnv(); + +const chat = new ChatClient({ + id: "conversation-123", + connection, // your OpenAI/SSE transport + persistence: upstashPersistence(redis), // <- that's it +}); + +await chat.sendMessage("In one short sentence, what is Upstash Redis?"); +``` + +The client now: + +- **Hydrates on construction** — calls `getItem(id)` and populates itself (overriding `initialMessages`). +- **Saves on every change** — calls `setItem(id, messages)` on each new message and streamed chunk, through an ordered write queue. +- **Clears on `clear()`** — calls `removeItem(id)`. + +## Try it + +Create a client, chat, then construct a **brand-new** client with the same `id` — it hydrates the full history from Redis with no `initialMessages`: + +```typescript +// Session 1 — persists to Redis +const a = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) }); +await a.sendMessage("In one short sentence, what is Upstash Redis?"); +await a.sendMessage("And in one sentence, what is TanStack?"); + +// Session 2 — same id, fresh client, no initialMessages +const b = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) }); +await b.sendMessage("What did I ask you first? Quote it back."); +// -> 'You asked: "In one short sentence, what is Upstash Redis?"' +``` + +Expected behavior: + +``` +Session 1: 4 messages stored under "tanstack:chat:demo" +Session 2: hydrates all 4 from Redis, then answers with full context +clear(): key removed from Redis +``` + +The second client never saw the first one's messages in memory - it recalled them from Redis, proving the conversation truly persisted. + +Persistence is best-effort: TanStack AI swallows adapter errors so storage hiccups never break the chat. Handle errors inside the adapter if you need to react to them. + +## Next steps + +- Check out [Agent Memory with Redis Search](/redis/tutorials/agent_memory) for more advanced retrieval. +- Set `ttlSeconds` to auto-expire idle conversations. +- Namespace keys per user, e.g. `prefix: \`chat:${userId}:\``. +- Swap the same adapter shape onto any TanStack AI client (React/Vue/Solid/Svelte `useChat`). \ No newline at end of file From ee4fcfbbdb4496dd3786b9d76fcdde79fc046b76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:23:07 +0000 Subject: [PATCH 2/4] chore(llms): regenerate llms.txt and llms-full.txt --- llms-full.txt | 1 + llms.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/llms-full.txt b/llms-full.txt index 629e1152f..392bbc434 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -28409,6 +28409,7 @@ from the list of the associated group and consumer. - [Serverless Redisson](https://upstash.com/docs/redis/tutorials/redisson.md): This tutorial shows how to use Upstash with Redisson client. - [Roadmap Voting App with Serverless Redis](https://upstash.com/docs/redis/tutorials/roadmapvotingapp.md): This is a single page application powered by upstash and next.js. - [Serverless API with Java and Redis](https://upstash.com/docs/redis/tutorials/serverless_java_redis.md) +- [TanStack AI Chat Persistance](https://upstash.com/docs/redis/tutorials/tanstack_chat_persistence.md): Use Upstash Redis to persist TanStack AI chat histories across reloads, navigation, and devices with a simple adapter. - [Using AWS SAM](https://upstash.com/docs/redis/tutorials/using_aws_sam.md) - [Serverless Redis on Google Cloud Functions](https://upstash.com/docs/redis/tutorials/using_google_cloud_functions.md) - [Using Serverless Framework](https://upstash.com/docs/redis/tutorials/using_serverless_framework.md) diff --git a/llms.txt b/llms.txt index c7a5b314f..f91ececce 100644 --- a/llms.txt +++ b/llms.txt @@ -817,6 +817,7 @@ - [Serverless Redisson](https://upstash.com/docs/redis/tutorials/redisson.md): This tutorial shows how to use Upstash with Redisson client. - [Roadmap Voting App with Serverless Redis](https://upstash.com/docs/redis/tutorials/roadmapvotingapp.md): This is a single page application powered by upstash and next.js. - [Serverless API with Java and Redis](https://upstash.com/docs/redis/tutorials/serverless_java_redis.md) +- [TanStack AI Chat Persistance](https://upstash.com/docs/redis/tutorials/tanstack_chat_persistence.md): Use Upstash Redis to persist TanStack AI chat histories across reloads, navigation, and devices with a simple adapter. - [Using AWS SAM](https://upstash.com/docs/redis/tutorials/using_aws_sam.md) - [Serverless Redis on Google Cloud Functions](https://upstash.com/docs/redis/tutorials/using_google_cloud_functions.md) - [Using Serverless Framework](https://upstash.com/docs/redis/tutorials/using_serverless_framework.md) From a3051b80fb1898e48b20128758314579252530cb Mon Sep 17 00:00:00 2001 From: CahidArda Date: Thu, 4 Jun 2026 17:29:19 +0300 Subject: [PATCH 3/4] fix: simplify args --- redis/tutorials/tanstack_chat_persistence.mdx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/redis/tutorials/tanstack_chat_persistence.mdx b/redis/tutorials/tanstack_chat_persistence.mdx index ad9a4a7d6..c010aee55 100644 --- a/redis/tutorials/tanstack_chat_persistence.mdx +++ b/redis/tutorials/tanstack_chat_persistence.mdx @@ -33,10 +33,14 @@ A persistence adapter is just an object with three methods. Each may be sync or import type { Redis } from "@upstash/redis"; import type { ChatClientPersistence, UIMessage } from "@tanstack/ai-client"; -export function upstashPersistence( - redis: Redis, - { prefix = "tanstack:chat:", ttlSeconds }: { prefix?: string; ttlSeconds?: number } = {}, -): ChatClientPersistence { +type UpstashPersistenceOptions = { + redis: Redis; + prefix?: string; + ttlSeconds?: number; +}; + +export function upstashPersistence(options: UpstashPersistenceOptions): ChatClientPersistence { + const { redis, prefix = "tanstack:chat:", ttlSeconds } = options; const key = (id: string) => `${prefix}${id}`; return { @@ -72,8 +76,8 @@ const redis = Redis.fromEnv(); const chat = new ChatClient({ id: "conversation-123", - connection, // your OpenAI/SSE transport - persistence: upstashPersistence(redis), // <- that's it + connection, // your OpenAI/SSE transport + persistence: upstashPersistence({ redis }), // <- that's it }); await chat.sendMessage("In one short sentence, what is Upstash Redis?"); @@ -91,12 +95,12 @@ Create a client, chat, then construct a **brand-new** client with the same `id` ```typescript // Session 1 — persists to Redis -const a = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) }); +const a = new ChatClient({ id: "demo", connection, persistence: upstashPersistence({ redis }) }); await a.sendMessage("In one short sentence, what is Upstash Redis?"); await a.sendMessage("And in one sentence, what is TanStack?"); // Session 2 — same id, fresh client, no initialMessages -const b = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) }); +const b = new ChatClient({ id: "demo", connection, persistence: upstashPersistence({ redis }) }); await b.sendMessage("What did I ask you first? Quote it back."); // -> 'You asked: "In one short sentence, what is Upstash Redis?"' ``` From 6cc6d7de376ecc2bd1015509c2e52a34c9cf21e6 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Thu, 4 Jun 2026 17:32:32 +0300 Subject: [PATCH 4/4] fix: link to original article --- redis/tutorials/tanstack_chat_persistence.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/tutorials/tanstack_chat_persistence.mdx b/redis/tutorials/tanstack_chat_persistence.mdx index c010aee55..88298ba73 100644 --- a/redis/tutorials/tanstack_chat_persistence.mdx +++ b/redis/tutorials/tanstack_chat_persistence.mdx @@ -3,7 +3,7 @@ title: TanStack AI Chat Persistance description: Use Upstash Redis to persist TanStack AI chat histories across reloads, navigation, and devices with a simple adapter. --- -By default a TanStack AI `ChatClient` keeps messages in memory only, so they vanish on reload. TanStack AI exposes a tiny persistence interface - `getItem` / `setItem` / `removeItem` - and any backend that implements it becomes durable storage. +By default a TanStack AI `ChatClient` keeps messages in memory only, so they vanish on reload. TanStack AI exposes a tiny [persistence interface](https://tanstack.com/ai/latest/docs/chat/persistence) - `getItem` / `setItem` / `removeItem` - and any backend that implements it becomes durable storage. Upstash Redis is a great fit: it's serverless with a REST API (no connection pooling, works in any edge/serverless runtime), latency is low enough to write on every streamed token, and per-conversation keys with an optional TTL give you free expiry of stale chats.