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 @usesatori/tools ai @ai-sdk/openai @clerk/nextjs
3

Set up environment variables

.env.local
# Satori
SATORI_API_KEY=sk_satori_...
SATORI_URL=https://api.usesatori.sh

# 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, 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

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

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