Every AI agent you have ever used forgets you the moment the conversation ends. Ask the same agent for a book recommendation tomorrow and it has no idea you already told it you hate thrillers. That gap between a chat session and actual continuity is the problem Mem0 solves.
In this tutorial you will wire Mem0's persistent memory layer into a Next.js application using Vercel AI SDK v5. By the end you will have a chatbot that remembers facts across sessions, personalizes responses based on past interactions, and handles multiple users with isolated memory stores.
Prerequisites
Before starting, make sure you have:
- Node.js 18 or later installed
- A Next.js 14 or 15 project (or create a new one below)
- A Mem0 API key — sign up at mem0.ai (free tier available)
- An OpenAI API key (or another provider supported by Vercel AI SDK)
- Familiarity with React, TypeScript, and Next.js App Router
What You Will Build
A personal AI assistant that:
- Remembers preferences expressed in conversation — dietary habits, favorite technologies, communication style
- Recalls past context on every new session without the user repeating themselves
- Isolates memory per user using user IDs, so each person gets their own memory store
- Exposes a memories panel so users can inspect and manage what the agent knows about them
Why Persistent Memory Matters for AI Agents
Today's LLMs are stateless by design. Every API call is independent. The standard workaround — stuffing the entire conversation history into the context window — breaks down for long-running assistants:
- Context windows fill up, forcing truncation
- Token costs grow linearly with conversation length
- Cross-session continuity is impossible without manual memory management
Mem0 solves this with a semantic memory layer that stores extracted facts, retrieves only the most relevant ones for each query, and keeps memory size bounded regardless of how long the user has been interacting with the assistant.
Step 1: Project Setup
Create a new Next.js project or use an existing one:
npx create-next-app@latest mem0-agent --typescript --tailwind --app
cd mem0-agentInstall the required packages:
npm install ai @mem0/vercel-ai-provider mem0ai
npm install @ai-sdk/openaiCreate a .env.local file in the project root:
MEM0_API_KEY=m0-your-key-here
OPENAI_API_KEY=sk-your-key-hereStep 2: Initialize the Mem0 Provider
Mem0 ships an official Vercel AI SDK provider called @mem0/vercel-ai-provider. It wraps any model with an automatic memory layer — memories are extracted from each message and injected as context on the next turn.
Create lib/mem0.ts:
import { createMem0 } from "@mem0/vercel-ai-provider";
export const mem0 = createMem0({
provider: "openai",
mem0ApiKey: process.env.MEM0_API_KEY!,
apiKey: process.env.OPENAI_API_KEY!,
});That is the entire setup. The provider handles both memory storage and retrieval automatically when you use it as a model in any Vercel AI SDK function.
Step 3: Build the Chat API Route
Create app/api/chat/route.ts:
import { streamText } from "ai";
import { mem0 } from "@/lib/mem0";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const { messages, userId } = await request.json();
const result = streamText({
model: mem0("gpt-4o", { user_id: userId }),
system: `You are a helpful personal assistant. Be concise and personalized.
Use what you know about the user to tailor your responses.`,
messages,
});
return result.toDataStreamResponse();
}The key difference from a standard Vercel AI SDK route is the user_id option passed to the model. Mem0 uses this to partition memories — each user ID maps to an isolated memory store. You can use any stable string: a Supabase user UUID, a Clerk user ID, or a session token.
Step 4: Manage Memories Directly
For finer control, use the mem0ai Node SDK alongside the provider. Create lib/memory-client.ts:
import MemoryClient from "mem0ai";
export const memoryClient = new MemoryClient({
apiKey: process.env.MEM0_API_KEY!,
});
export async function addMemories(
messages: Array<{ role: string; content: string }>,
userId: string
) {
return memoryClient.add(messages, { userId });
}
export async function searchMemories(query: string, userId: string) {
return memoryClient.search(query, {
filters: { userId },
limit: 10,
});
}
export async function getAllMemories(userId: string) {
return memoryClient.getAll({ filters: { userId } });
}
export async function deleteMemory(memoryId: string) {
return memoryClient.delete(memoryId);
}Now create an API route for memory management at app/api/memories/route.ts:
import { NextRequest, NextResponse } from "next/server";
import {
addMemories,
searchMemories,
getAllMemories,
deleteMemory,
} from "@/lib/memory-client";
export async function GET(request: NextRequest) {
const userId = request.nextUrl.searchParams.get("userId");
const query = request.nextUrl.searchParams.get("query");
if (!userId) {
return NextResponse.json({ error: "userId required" }, { status: 400 });
}
const memories = query
? await searchMemories(query, userId)
: await getAllMemories(userId);
return NextResponse.json({ memories });
}
export async function DELETE(request: NextRequest) {
const { memoryId } = await request.json();
await deleteMemory(memoryId);
return NextResponse.json({ success: true });
}Step 5: Build the Chat Interface
Create components/Chat.tsx:
"use client";
import { useChat } from "ai/react";
import { useState } from "react";
interface ChatProps {
userId: string;
}
export default function Chat({ userId }: ChatProps) {
const [input, setInput] = useState("");
const { messages, append, isLoading } = useChat({
api: "/api/chat",
body: { userId },
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
append({ role: "user", content: input });
setInput("");
};
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<p className="text-gray-400 text-center mt-8">
Start a conversation. I will remember you between sessions.
</p>
)}
{messages.map((m) => (
<div
key={m.id}
className={`flex ${m.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
m.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-800 text-gray-100"
}`}
>
{m.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-800 rounded-2xl px-4 py-2 text-gray-400">
Thinking...
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t border-gray-700">
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-gray-800 rounded-xl px-4 py-2 text-white outline-none"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-6 py-2 rounded-xl"
>
Send
</button>
</div>
</form>
</div>
);
}Step 6: Add a Memories Panel
Giving users visibility into what the agent knows about them builds trust. Create components/MemoriesPanel.tsx:
"use client";
import { useEffect, useState } from "react";
interface Memory {
id: string;
memory: string;
created_at: string;
}
export default function MemoriesPanel({ userId }: { userId: string }) {
const [memories, setMemories] = useState<Memory[]>([]);
const [loading, setLoading] = useState(true);
const fetchMemories = async () => {
setLoading(true);
const res = await fetch(`/api/memories?userId=${userId}`);
const data = await res.json();
setMemories(data.memories || []);
setLoading(false);
};
useEffect(() => {
fetchMemories();
}, [userId]);
const handleDelete = async (memoryId: string) => {
await fetch("/api/memories", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ memoryId }),
});
setMemories((prev) => prev.filter((m) => m.id !== memoryId));
};
if (loading) return <p className="text-gray-400 p-4">Loading memories...</p>;
return (
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">What I Know About You</h2>
<button
onClick={fetchMemories}
className="text-sm text-blue-400 hover:underline"
>
Refresh
</button>
</div>
{memories.length === 0 ? (
<p className="text-gray-400 text-sm">
No memories yet. Start chatting to build your profile.
</p>
) : (
<ul className="space-y-2">
{memories.map((m) => (
<li
key={m.id}
className="bg-gray-800 rounded-lg p-3 flex items-start justify-between gap-2"
>
<span className="text-sm text-gray-200">{m.memory}</span>
<button
onClick={() => handleDelete(m.id)}
className="text-red-400 hover:text-red-300 text-xs shrink-0"
>
Delete
</button>
</li>
))}
</ul>
)}
</div>
);
}Step 7: Wire It All Together
Update app/page.tsx to compose the interface:
import Chat from "@/components/Chat";
import MemoriesPanel from "@/components/MemoriesPanel";
// In a real app, derive userId from your auth session (Clerk, NextAuth, Supabase, etc.)
const USER_ID = "demo-user-001";
export default function Home() {
return (
<div className="flex h-screen bg-gray-900 text-white">
<div className="flex-1 flex flex-col max-w-2xl mx-auto border-x border-gray-700">
<header className="p-4 border-b border-gray-700">
<h1 className="text-xl font-bold">Personal AI Assistant</h1>
<p className="text-sm text-gray-400">Powered by Mem0 + Vercel AI SDK</p>
</header>
<Chat userId={USER_ID} />
</div>
<aside className="w-72 border-l border-gray-700 overflow-y-auto">
<MemoriesPanel userId={USER_ID} />
</aside>
</div>
);
}Step 8: Multi-User Memory in Production
In a production application, replace the hardcoded USER_ID with a real authentication identity. Here is an example using Next.js with NextAuth:
// app/api/chat/route.ts
import { auth } from "@/auth";
import { streamText } from "ai";
import { mem0 } from "@/lib/mem0";
export async function POST(request: Request) {
const session = await auth();
const userId = session?.user?.id;
if (!userId) {
return new Response("Unauthorized", { status: 401 });
}
const { messages } = await request.json();
const result = streamText({
model: mem0("gpt-4o", { user_id: userId }),
system: "You are a helpful personal assistant with memory.",
messages,
});
return result.toDataStreamResponse();
}Mem0 guarantees that memories for userId: "alice" never leak to userId: "bob". Each user's memory store is fully isolated.
Step 9: Fine-Tuning Memory Behavior
Mem0 lets you configure what gets stored. Pass options when initializing the provider to control memory sensitivity:
export const mem0 = createMem0({
provider: "openai",
mem0ApiKey: process.env.MEM0_API_KEY!,
apiKey: process.env.OPENAI_API_KEY!,
// Optional: use a specific org/project in the Mem0 platform
organizationName: "my-org",
projectName: "personal-assistant",
});You can also add memories manually at any point — for example, when a user fills in a profile form:
await memoryClient.add(
[
{
role: "user",
content: "My name is Sara. I am a data scientist in Tunis.",
},
],
{ userId: "sara-123" }
);Those facts are available to the agent on the very next request, before Sara has said a single word in chat.
Testing Your Implementation
Run the dev server:
npm run devOpen http://localhost:3000. Tell the assistant a few facts about yourself:
- "I am vegetarian and I prefer Python for data science work."
- "My timezone is UTC+1 and I usually work late."
- "I dislike overly formal writing."
Refresh the page to start a new session. Ask the assistant for a meal suggestion or a coding tip — it should use the facts you shared in the previous session without you having to repeat them.
Check the Memories Panel on the right side. You should see the extracted facts listed there, each one deletable.
Troubleshooting
Memories not appearing after chat
- Confirm your
MEM0_API_KEYis set correctly in.env.local - Check the Mem0 dashboard at app.mem0.ai to verify memory writes are arriving
- Ensure you are passing the same
userIdconsistently
Context not injected into responses
- The
@mem0/vercel-ai-providerprovider automatically injects memories. If responses seem generic, try asking something that directly references past facts. - Memory retrieval is semantic, not exact-match. Rephrase your question if needed.
TypeScript errors on createMem0
- Make sure you are on
@mem0/vercel-ai-providerversion 1.0.5 or later which ships full TypeScript types. - Run
npm install @mem0/vercel-ai-provider@latestto update.
Rate limits on the free tier
- Mem0's free tier allows up to 500 memory operations per month. For a production app, upgrade to a paid plan or self-host the open-source version from the Mem0 GitHub repo.
Next Steps
Now that you have a working stateful AI agent, here are natural extensions:
- Add vector search for memories — use
memoryClient.search()with a query to retrieve only the most relevant memories for complex domains - Combine with RAG — pair Mem0 memory with a document retrieval system for agents that know both user preferences and domain content
- Memory categories — tag memories with metadata such as
category: "work"orcategory: "personal"and filter per context - Session summaries — at the end of each session, ask the LLM to produce a summary and store it as a memory for long-form continuity
- Self-hosted Mem0 — run the open-source version on your own infrastructure for full data sovereignty using the Docker image from the official repo
Conclusion
Stateless agents are useful; stateful agents are indispensable. With Mem0 and Vercel AI SDK v5, adding a persistent memory layer to your Next.js application takes under 30 minutes of setup and a handful of TypeScript files. The result is an assistant that grows more useful with every conversation — remembering the user's preferences, context, and history without you managing a single database table manually.
Start small: deploy the basic chat route, verify memories are being stored, then expand with the memories panel and multi-user isolation. Each step compounds into a significantly better user experience.