Add memoryMiddleware to a working chat route. The route recalls memory before each answer and saves each turn.
npm i @tanstack/ai-memorypnpm add @tanstack/ai-memoryyarn add @tanstack/ai-memorybun add @tanstack/ai-memoryStart with inMemory(). It keeps records in a Map in the server process:
// memory.ts
import { inMemory } from '@tanstack/ai-memory/in-memory'
export const memory = inMemory()Set the scope from your auth session. The onRecall log lets you see memory work:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { memoryMiddleware } from '@tanstack/ai-memory'
import { memory } from './memory'
// Replace with your auth. Read the user from the session, never from the body.
function getUserId(_request: Request): string {
return 'demo-user'
}
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const userId = getUserId(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
middleware: [
memoryMiddleware({
adapter: memory,
// In a real app, make sure that this thread belongs to userId.
scope: { threadId: params.threadId, userId },
onRecall: ({ result }) => {
console.log('memory recalled', result.fragments?.length ?? 0, 'items')
},
}),
],
})
return toServerSentEventsResponse(stream)
}Memory runs on the server only. The client is a normal chat client:
import { useState } from 'react'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === 'text' ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault()
void sendMessage(input)
setInput('')
}}
>
<input value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit">Send</button>
</form>
</>
)
}For Vue, Solid, Svelte, and the other frameworks, use the client from the Quick Start. Nothing in it changes for memory.
The server log shows memory recalled 2 items (or a similar count) for the second message. The first turn was saved and then recalled into the prompt.
redis() keeps memory across restarts and shares it between server processes. Change only memory.ts. The route stays the same:
// memory.ts
import Redis from 'ioredis'
import { redis } from '@tanstack/ai-memory/redis'
const client = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379')
export const memory = redis({ redis: client })Install ioredis (or redis) yourself. Both are optional peer dependencies. For node-redis and the other options, see redis().
To use a hosted service, export hindsight(), mem0(), or honcho() from memory.ts instead. See Adapters.
Word overlap misses a question that uses different words than the stored text. Add an embedder to inMemory() or redis() to also score by meaning:
// memory.ts
import OpenAI from 'openai'
import { inMemory } from '@tanstack/ai-memory/in-memory'
const openai = new OpenAI()
export const memory = inMemory({
embedder: {
async embed(text) {
const result = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
const embedding = result.data[0]?.embedding
if (!embedding) throw new Error('embedding request returned no vector')
return embedding
},
},
})The embedder runs on each recall (for the query) and on each save (for the stored text).
Your route now remembers what the user says and can move to Redis with a one-file change. To log memory in production and watch it in devtools, see Operating memory.