Skip to content

Commit 702dca1

Browse files
committed
feat: add TanStack AI chat persistence tutorial using Upstash Redis
1 parent 823a9d2 commit 702dca1

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@
868868
"redis/tutorials/python_url_shortener",
869869
"redis/tutorials/api_with_cdk",
870870
"redis/tutorials/agent_memory",
871+
"redis/tutorials/tanstack_chat_persistence",
871872
"redis/tutorials/analytics_with_redis",
872873
"redis/tutorials/auto_complete_with_serverless_redis",
873874
"redis/tutorials/aws_app_runner_with_redis",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: TanStack AI Chat Persistance
3+
description: Use Upstash Redis to persist TanStack AI chat histories across reloads, navigation, and devices with a simple adapter.
4+
---
5+
6+
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.
7+
8+
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.
9+
10+
<Note>This tutorial uses OpenAI for the model, but persistence is model-agnostic.</Note>
11+
12+
## Prerequisites
13+
14+
- An [Upstash Redis](https://console.upstash.com) database
15+
- A TanStack AI `ChatClient` (`@tanstack/ai-client`)
16+
- `@upstash/redis`
17+
18+
```bash
19+
npm install @tanstack/ai-client @upstash/redis
20+
```
21+
22+
```bash
23+
UPSTASH_REDIS_REST_URL="https://..."
24+
UPSTASH_REDIS_REST_TOKEN="..."
25+
```
26+
27+
## The adapter
28+
29+
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.
30+
31+
```typescript
32+
// upstash-persistence.ts
33+
import type { Redis } from "@upstash/redis";
34+
import type { ChatClientPersistence, UIMessage } from "@tanstack/ai-client";
35+
36+
export function upstashPersistence(
37+
redis: Redis,
38+
{ prefix = "tanstack:chat:", ttlSeconds }: { prefix?: string; ttlSeconds?: number } = {},
39+
): ChatClientPersistence {
40+
const key = (id: string) => `${prefix}${id}`;
41+
42+
return {
43+
async getItem(id) {
44+
const stored = await redis.get<Array<UIMessage>>(key(id));
45+
if (!stored) return null;
46+
// createdAt round-trips as a string through JSON; revive it.
47+
return stored.map((m) => ({
48+
...m,
49+
createdAt: typeof m.createdAt === "string" ? new Date(m.createdAt) : m.createdAt,
50+
}));
51+
},
52+
async setItem(id, messages) {
53+
await redis.set(key(id), messages, ttlSeconds ? { ex: ttlSeconds } : undefined);
54+
},
55+
async removeItem(id) {
56+
await redis.del(key(id));
57+
},
58+
};
59+
}
60+
```
61+
62+
## Use it
63+
64+
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.
65+
66+
```typescript
67+
import { Redis } from "@upstash/redis";
68+
import { ChatClient } from "@tanstack/ai-client";
69+
import { upstashPersistence } from "./upstash-persistence";
70+
71+
const redis = Redis.fromEnv();
72+
73+
const chat = new ChatClient({
74+
id: "conversation-123",
75+
connection, // your OpenAI/SSE transport
76+
persistence: upstashPersistence(redis), // <- that's it
77+
});
78+
79+
await chat.sendMessage("In one short sentence, what is Upstash Redis?");
80+
```
81+
82+
The client now:
83+
84+
- **Hydrates on construction** — calls `getItem(id)` and populates itself (overriding `initialMessages`).
85+
- **Saves on every change** — calls `setItem(id, messages)` on each new message and streamed chunk, through an ordered write queue.
86+
- **Clears on `clear()`** — calls `removeItem(id)`.
87+
88+
## Try it
89+
90+
Create a client, chat, then construct a **brand-new** client with the same `id` — it hydrates the full history from Redis with no `initialMessages`:
91+
92+
```typescript
93+
// Session 1 — persists to Redis
94+
const a = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) });
95+
await a.sendMessage("In one short sentence, what is Upstash Redis?");
96+
await a.sendMessage("And in one sentence, what is TanStack?");
97+
98+
// Session 2 — same id, fresh client, no initialMessages
99+
const b = new ChatClient({ id: "demo", connection, persistence: upstashPersistence(redis) });
100+
await b.sendMessage("What did I ask you first? Quote it back.");
101+
// -> 'You asked: "In one short sentence, what is Upstash Redis?"'
102+
```
103+
104+
Expected behavior:
105+
106+
```
107+
Session 1: 4 messages stored under "tanstack:chat:demo"
108+
Session 2: hydrates all 4 from Redis, then answers with full context
109+
clear(): key removed from Redis
110+
```
111+
112+
The second client never saw the first one's messages in memory - it recalled them from Redis, proving the conversation truly persisted.
113+
114+
<Note>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.</Note>
115+
116+
## Next steps
117+
118+
- Check out [Agent Memory with Redis Search](/redis/tutorials/agent_memory) for more advanced retrieval.
119+
- Set `ttlSeconds` to auto-expire idle conversations.
120+
- Namespace keys per user, e.g. `prefix: \`chat:${userId}:\``.
121+
- Swap the same adapter shape onto any TanStack AI client (React/Vue/Solid/Svelte `useChat`).

0 commit comments

Comments
 (0)