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
Create a Next.js project
npx create-next-app@latest my-memory-app
cd my-memory-app
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
Authentication Setup
Configure Clerk
middleware.ts
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
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
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { memoryTools, getContext } from '@usesatori/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 getContext(
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_item 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
'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_item' && (
<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
'use client';
import { useEffect, useState } from 'react';
import { useUser } from '@clerk/nextjs';
import { MemoryClient } from '@usesatori/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
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@usesatori/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
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@usesatori/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
import { auth } from '@clerk/nextjs/server';
import { MemoryClient } from '@usesatori/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
Visit the application
Open http://localhost:3000 in your browser
Start chatting
Try these prompts:
- “Remember that I love TypeScript”
- “My favorite color is blue”
- “What do you know about me?”
Next Steps
Deploy to Vercel
Deploy your application to production
Add More Features
Explore advanced examples
API Reference
Learn about all available endpoints
Troubleshooting
Fix common issues