Skip to main content

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

npx create-next-app@latest my-memory-app
cd my-memory-app
Choose these options:
  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • App Router: Yes
2

Install dependencies

npm install @satori/tools ai @ai-sdk/openai @clerk/nextjs
3

Set up environment variables

.env.local
# 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
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, 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
'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
'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
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
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
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

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

Next Steps