Overview
This guide walks you through building a complete Next.js application with Satori memory, including:- Chat interface with memory
- User authentication
- Memory dashboard
- API routes with proper error handling
Project Setup
1
Create a Next.js project
Copy
npx create-next-app@latest my-memory-app
cd my-memory-app
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
2
Install dependencies
Copy
npm install @satori/tools ai @ai-sdk/openai @clerk/nextjs
3
Set up environment variables
.env.local
Copy
# Satori
SATORI_API_KEY=sk_satori_...
SATORI_URL=https://api.satori.dev
# OpenAI
OPENAI_API_KEY=sk-...
# Clerk (for authentication)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
Authentication Setup
Configure Clerk
middleware.ts
Copy
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
};
Root Layout
app/layout.tsx
Copy
import { ClerkProvider } from '@clerk/nextjs';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Chat API Route
Create a memory-enabled chat endpoint:app/api/chat/route.ts
Copy
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { memoryTools, getMemoryContext } from '@satori/tools';
import { auth } from '@clerk/nextjs/server';
export async function POST(req: Request) {
try {
// Authenticate user
const { userId } = await auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
// Parse request
const { messages } = await req.json();
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return new Response('Invalid request', { status: 400 });
}
const userMessage = messages[messages.length - 1].content;
// Configure memory
const memoryConfig = {
apiKey: process.env.SATORI_API_KEY!,
baseUrl: process.env.SATORI_URL!,
userId,
};
// Create memory tools
const tools = memoryTools(memoryConfig);
// Pre-fetch relevant context
let memoryContext = '';
try {
memoryContext = await getMemoryContext(
memoryConfig,
userMessage,
{ limit: 5 }
);
} catch (error) {
console.error('Failed to fetch memory context:', error);
// Continue without context
}
// Stream response with memory
const result = await streamText({
model: openai('gpt-4o'),
system: `You are a helpful assistant with long-term memory.
${memoryContext ? `What you know about this user:\n${memoryContext}\n` : ''}
When the user shares important information, use the add_memory tool to save it.
Be natural and conversational. Don't explicitly mention that you're saving memories.`,
messages,
tools,
maxSteps: 5,
});
return result.toDataStreamResponse();
} catch (error) {
console.error('Chat error:', error);
return new Response(
JSON.stringify({ error: 'Failed to process message' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
}
Chat Page
Create an interactive chat interface:app/chat/page.tsx
Copy
'use client';
import { useChat } from 'ai/react';
import { useUser } from '@clerk/nextjs';
import { useState } from 'react';
export default function ChatPage() {
const { user, isLoaded } = useUser();
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
const [showMemorySaved, setShowMemorySaved] = useState(false);
if (!isLoaded) {
return <div className="flex items-center justify-center h-screen">Loading...</div>;
}
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4 pb-4 border-b">
<div>
<h1 className="text-2xl font-bold">Chat with Memory</h1>
<p className="text-sm text-gray-500">
Signed in as {user?.firstName || user?.emailAddresses[0].emailAddress}
</p>
</div>
<a
href="/dashboard"
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
>
View Memories
</a>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.length === 0 && (
<div className="text-center text-gray-500 mt-8">
<p className="text-lg mb-2">Start a conversation!</p>
<p className="text-sm">Try saying: "Remember that I love TypeScript"</p>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] p-4 rounded-lg ${
message.role === 'user'
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-900'
}`}
>
<p className="text-sm font-semibold mb-1">
{message.role === 'user' ? 'You' : 'Assistant'}
</p>
<p className="whitespace-pre-wrap">{message.content}</p>
{/* Show tool calls */}
{message.toolInvocations?.map((tool, i) => (
<div key={i} className="mt-2 text-xs opacity-75">
{tool.toolName === 'add_memory' && (
<span>💾 Saved to memory</span>
)}
</div>
))}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 p-4 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send
</button>
</form>
</div>
);
}
Memory Dashboard
Create a page to view and manage memories:app/dashboard/page.tsx
Copy
'use client';
import { useEffect, useState } from 'react';
import { useUser } from '@clerk/nextjs';
import { MemoryClient } from '@satori/tools';
interface Memory {
id: string;
content: string;
createdAt: string;
metadata?: Record<string, unknown>;
}
export default function DashboardPage() {
const { user, isLoaded } = useUser();
const [memories, setMemories] = useState<Memory[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
if (isLoaded && user) {
loadMemories();
}
}, [isLoaded, user]);
async function loadMemories() {
try {
const response = await fetch('/api/memories');
const data = await response.json();
setMemories(data.memories || []);
} catch (error) {
console.error('Failed to load memories:', error);
} finally {
setLoading(false);
}
}
async function deleteMemory(id: string) {
if (!confirm('Are you sure you want to delete this memory?')) {
return;
}
try {
await fetch(`/api/memories/${id}`, { method: 'DELETE' });
setMemories(memories.filter((m) => m.id !== id));
} catch (error) {
console.error('Failed to delete memory:', error);
}
}
async function searchMemories() {
if (!searchQuery.trim()) {
loadMemories();
return;
}
try {
const response = await fetch(
`/api/memories/search?q=${encodeURIComponent(searchQuery)}`
);
const data = await response.json();
setMemories(data.memories || []);
} catch (error) {
console.error('Failed to search memories:', error);
}
}
if (!isLoaded || loading) {
return <div className="flex items-center justify-center h-screen">Loading...</div>;
}
return (
<div className="max-w-4xl mx-auto p-4">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Your Memories</h1>
<p className="text-gray-500">
{memories.length} {memories.length === 1 ? 'memory' : 'memories'} stored
</p>
</div>
<a
href="/chat"
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
>
Back to Chat
</a>
</div>
{/* Search */}
<div className="mb-6 flex gap-2">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && searchMemories()}
placeholder="Search memories..."
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<button
onClick={searchMemories}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Search
</button>
{searchQuery && (
<button
onClick={() => {
setSearchQuery('');
loadMemories();
}}
className="px-4 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
>
Clear
</button>
)}
</div>
{/* Memories List */}
<div className="space-y-4">
{memories.length === 0 ? (
<div className="text-center text-gray-500 py-12">
<p className="text-lg mb-2">No memories yet</p>
<p className="text-sm">Start chatting to create some memories!</p>
</div>
) : (
memories.map((memory) => (
<div
key={memory.id}
className="p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<p className="text-gray-900 mb-2">{memory.content}</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>
{new Date(memory.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
<button
onClick={() => deleteMemory(memory.id)}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
</div>
</div>
))
)}
</div>
</div>
);
}
Memory API Routes
Get All Memories
app/api/memories/route.ts
Copy
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@satori/tools';
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
const client = new MemoryClient({
apiKey: process.env.SATORI_API_KEY!,
baseUrl: process.env.SATORI_URL!,
userId,
});
const memories = await client.getAllMemories();
return Response.json({ memories });
} catch (error) {
console.error('Failed to fetch memories:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
Search Memories
app/api/memories/search/route.ts
Copy
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@satori/tools';
export async function GET(req: Request) {
try {
const { userId } = await auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
const { searchParams } = new URL(req.url);
const query = searchParams.get('q');
if (!query) {
return new Response('Missing query parameter', { status: 400 });
}
const client = new MemoryClient({
apiKey: process.env.SATORI_API_KEY!,
baseUrl: process.env.SATORI_URL!,
userId,
});
const memories = await client.searchMemories(query);
return Response.json({ memories });
} catch (error) {
console.error('Failed to search memories:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
Delete Memory
app/api/memories/[id]/route.ts
Copy
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@satori/tools';
export async function DELETE(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
const client = new MemoryClient({
apiKey: process.env.SATORI_API_KEY!,
baseUrl: process.env.SATORI_URL!,
userId,
});
await client.deleteMemory(params.id);
return Response.json({ success: true });
} catch (error) {
console.error('Failed to delete memory:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
Running the Application
1
Start the development server
Copy
npm run dev
2
Visit the application
Open http://localhost:3000 in your browser
3
Sign in
Create an account or sign in with Clerk
4
Start chatting
Try these prompts:
- “Remember that I love TypeScript”
- “My favorite color is blue”
- “What do you know about me?”
5
View your memories
Click “View Memories” to see all stored memories