writing/tutorial/2026/05
TutorialMay 22, 2026·25 min read

Build Stateful AI Agents with Persistent Memory Using Mem0 and Vercel AI SDK

Learn how to add persistent, personalized memory to your AI agents using Mem0 and Vercel AI SDK v5 in Next.js. This tutorial covers setup, memory storage, retrieval, multi-user sessions, and building a production-ready stateful chatbot.

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:

  1. Remembers preferences expressed in conversation — dietary habits, favorite technologies, communication style
  2. Recalls past context on every new session without the user repeating themselves
  3. Isolates memory per user using user IDs, so each person gets their own memory store
  4. 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-agent

Install the required packages:

npm install ai @mem0/vercel-ai-provider mem0ai
npm install @ai-sdk/openai

Create a .env.local file in the project root:

MEM0_API_KEY=m0-your-key-here
OPENAI_API_KEY=sk-your-key-here

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

Open http://localhost:3000. Tell the assistant a few facts about yourself:

  1. "I am vegetarian and I prefer Python for data science work."
  2. "My timezone is UTC+1 and I usually work late."
  3. "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_KEY is 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 userId consistently

Context not injected into responses

  • The @mem0/vercel-ai-provider provider 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-provider version 1.0.5 or later which ships full TypeScript types.
  • Run npm install @mem0/vercel-ai-provider@latest to 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" or category: "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.