Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
125 changes: 125 additions & 0 deletions redis/tutorials/tanstack_chat_persistence.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
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](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.

<Note>This tutorial uses OpenAI for the model, but persistence is model-agnostic.</Note>

## 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";

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 {
async getItem(id) {
const stored = await redis.get<Array<UIMessage>>(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.

<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>

## 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`).
Loading