Getting Started with Cortex Memory for Vercel AI SDK
Complete guide to adding persistent memory to your Next.js application using Cortex Memory SDK v0.21.0+.
Prerequisites
- Node.js 18+
- Next.js 14+ (App Router)
- Convex account (free tier available)
- OpenAI API key (or other LLM provider)
Option 1: Run the Quickstart Demo (Recommended)
The fastest way to understand Cortex Memory is with our interactive quickstart demo. This is a complete Next.js app with:
- Real-time layer flow visualization
- Live memory orchestration
- Multi-tenant memory space switching
- Streaming with progressive fact extraction
- Authentication system
- Data preview panels
$ cd packages/vercel-ai-provider/quickstart
npm install
$ cp .env.local.example .env.local
Edit .env.local with your Convex URL and OpenAI key.
$ npm run convex:dev
$ npm run dev
Open http://localhost:3000 and:
- Send a message like "Hi, I'm Alex and I work at Acme Corp as an engineer"
- Watch the Layer Flow Diagram show data flowing through all 7 Cortex layers in real-time
- Ask a question like "What do you remember about me?"
- Refresh the page and see memory persists across sessions
- Try fact updates - say "I actually prefer purple" after mentioning "I like blue" to see belief revision
Option 2: Add to Existing Project
Step 1: Install Dependencies
$ npm install @cortexmemory/vercel-ai-provider @cortexmemory/sdk ai convex
npm install @ai-sdk/openai
You can also use @ai-sdk/anthropic, @ai-sdk/google, or other providers.
Step 2: Set Up Convex Backend
$ npm create cortex-memories
Follow the wizard to set up Convex with Cortex backend automatically.
Step 2.5: Deploy Convex Schema
After setting up Convex, deploy the schema:
$ npm run convex:dev
This initializes the Convex backend and is required before running your app.
Step 3: Configure Environment Variables
Create .env.local:
# Required
CONVEX_URL=https://your-deployment.convex.cloud
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
OPENAI_API_KEY=sk-your-key-here
# Optional: Memory Space
MEMORY_SPACE_ID=my-chatbot
# Optional: Enable fact extraction (SDK v0.18.0+)
CORTEX_FACT_EXTRACTION=true
CORTEX_FACT_EXTRACTION_MODEL=gpt-4o-mini
# Optional: Enable graph sync (SDK v0.19.0+)
# CORTEX_GRAPH_SYNC=true
# NEO4J_URI=bolt://localhost:7687
# NEO4J_USERNAME=neo4j
# NEO4J_PASSWORD=your-password
Step 4: Create Memory-Enabled Chat API
// app/api/chat/route.ts
import { createCortexMemoryAsync } from "@cortexmemory/vercel-ai-provider";
import { openai, createOpenAI } from "@ai-sdk/openai";
import {
streamText,
embed,
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
} from "ai";
// Create OpenAI client for embeddings
const openaiClient = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: Request) {
const { messages, conversationId: providedConversationId } = await req.json();
// Generate conversation ID if not provided (new chat)
const conversationId =
providedConversationId ||
`conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
// Normalize messages for AI SDK v6 compatibility
// This ensures messages have the `parts` array format expected by convertToModelMessages
const normalizedMessages = messages.map((msg: any) => {
const role = msg.role === "agent" ? "assistant" : msg.role;
const parts = msg.parts || (msg.content ? [{ type: "text", text: msg.content }] : []);
return { ...msg, role, parts };
});
// Convert UIMessage[] to ModelMessage[] for streamText
const modelMessagesResult = convertToModelMessages(normalizedMessages);
const modelMessages =
modelMessagesResult instanceof Promise
? await modelMessagesResult
: modelMessagesResult;
// Create memory-augmented model with async initialization
// Use createCortexMemoryAsync for graph memory support
const cortexMemory = await createCortexMemoryAsync({
convexUrl: process.env.CONVEX_URL!,
memorySpaceId: process.env.MEMORY_SPACE_ID || "default-chat",
// User identification
userId: "demo-user", // Replace with real user ID
userName: "User",
// REQUIRED in SDK v0.17.0+
agentId: "my-assistant",
agentName: "My AI Assistant",
// Conversation ID for chat history isolation
conversationId,
// Embedding provider (required for semantic deduplication)
embeddingProvider: {
generate: async (text: string) => {
const result = await embed({
model: openaiClient.embedding("text-embedding-3-small"),
value: text,
});
return result.embedding;
},
},
// Optional: Enable enhanced features
enableFactExtraction: process.env.CORTEX_FACT_EXTRACTION === "true",
enableGraphMemory: process.env.CORTEX_GRAPH_SYNC === "true",
});
// Stream response with memory integration
const result = await streamText({
model: cortexMemory(openai("gpt-4o-mini")),
messages: modelMessages,
system: `You are a helpful assistant with long-term memory.
You remember everything users tell you and can recall it naturally.`,
});
// Use createUIMessageStreamResponse for advanced streaming features
return createUIMessageStreamResponse({
stream: createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(result.toUIMessageStream());
},
}),
});
}
Step 5: Create Chat UI
// app/page.tsx
'use client';
import { useChat } from 'ai/react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div className="p-4 max-w-2xl mx-auto">
<h1 className="text-2xl mb-4">Chat with Memory</h1>
<div className="space-y-4 mb-4">
{messages.map(m => (
<div
key={m.id}
className={`p-3 rounded ${
m.role === 'user' ? 'bg-blue-100' : 'bg-gray-100'
}`}
>
<strong>{m.role === 'user' ? 'You' : 'Assistant'}:</strong> {m.content}
</div>
))}
{isLoading && (
<div className="p-3 rounded bg-gray-100 animate-pulse">
Thinking...
</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
className="flex-1 p-2 border rounded"
disabled={isLoading}
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
disabled={isLoading}
>
Send
</button>
</form>
</div>
);
}
Step 6: Test Memory
- Start your app:
$ npm run dev
-
Test memory persistence:
- Say: "Hi, my name is Alice and I work at Acme Corp"
- Agent: "Nice to meet you, Alice! Acme Corp sounds great."
- Refresh the page
- Ask: "What do you know about me?"
- Agent: "I remember you're Alice and you work at Acme Corp!" ✨
Configuration Options
Required Options
| Option | Type | Description |
|---|---|---|
convexUrl | string | Your Convex deployment URL |
memorySpaceId | string | Namespace for memory isolation |
userId | string | User identifier |
agentId | string | Agent identifier (required in v0.17.0+) |
conversationId | string | Conversation identifier for chat isolation |
embeddingProvider | object | Embedding provider for semantic deduplication (required for belief revision) |
Optional Options
| Option | Type | Default | Description |
|---|---|---|---|
userName | string | "User" | User display name |
agentName | string | agentId | Agent display name |
enableFactExtraction | boolean | false | Enable LLM fact extraction |
enableGraphMemory | boolean | false | Sync to graph database |
memorySearchLimit | number | 20 | Max memories to retrieve |
minMemoryRelevance | number | 0.7 | Min similarity score (0-1) |
debug | boolean | false | Enable debug logging |
Use createCortexMemoryAsync (with await) instead of createCortexMemory when you need graph memory support or want automatic configuration from environment variables.
Dynamic User Resolution
For authentication systems:
import { auth } from "@clerk/nextjs";
import { createCortexMemoryAsync } from "@cortexmemory/vercel-ai-provider";
export async function POST(req: Request) {
const { userId } = await auth();
if (!userId) {
return new Response("Unauthorized", { status: 401 });
}
const cortexMemory = await createCortexMemoryAsync({
convexUrl: process.env.CONVEX_URL!,
memorySpaceId: "app",
userId,
userName: "User",
agentId: "app-assistant",
conversationId: `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
});
// ... use cortexMemory
}
Multi-Tenant Setup
Different memory space per team/tenant:
import { createCortexMemoryAsync } from "@cortexmemory/vercel-ai-provider";
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
export async function POST(req: Request) {
const { teamId, messages, conversationId } = await req.json();
const cortexMemory = await createCortexMemoryAsync({
convexUrl: process.env.CONVEX_URL!,
memorySpaceId: `team-${teamId}`, // Isolated per team
userId: currentUser.id,
userName: currentUser.name,
agentId: "team-assistant",
conversationId: conversationId || `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
});
const result = await streamText({
model: cortexMemory(openai("gpt-4o-mini")),
messages,
});
return result.toDataStreamResponse();
}
Deploy to Vercel
$ vercel
Set environment variables in Vercel dashboard:
CONVEX_URLNEXT_PUBLIC_CONVEX_URLOPENAI_API_KEYMEMORY_SPACE_ID(optional)CORTEX_FACT_EXTRACTION(optional)CORTEX_GRAPH_SYNC(optional)
Troubleshooting
"agentId is required"
Add agentId to your configuration (required in SDK v0.17.0+):
const cortexMemory = await createCortexMemoryAsync({
// ... other config
agentId: "my-assistant", // Add this!
});
AI SDK v6 Message Format
If you encounter issues with message formatting, ensure you normalize messages for AI SDK v6 compatibility:
// Normalize messages to ensure they have the `parts` array format
const normalizedMessages = messages.map((msg: any) => {
const role = msg.role === "agent" ? "assistant" : msg.role;
const parts = msg.parts || (msg.content ? [{ type: "text", text: msg.content }] : []);
return { ...msg, role, parts };
});
// Convert to ModelMessage[] format
const modelMessages = await convertToModelMessages(normalizedMessages);
Memory not persisting
- Check that Convex is running:
npx convex dev - Verify
CONVEX_URLis set correctly - Check browser console for errors
Search returns no results
- Make sure you've had at least one conversation first
- If using semantic search, set up
embeddingProvider - Check that
memorySpaceIdmatches between requests
See the Troubleshooting Guide for more solutions.