Skip to main content

Overview

Satori uses API keys for authentication, powered by Clerk’s API key management system. Each API key is tied to a specific tenant (you, the developer), and all memories stored using that key are isolated to your account.

API Key Structure

Satori API keys follow this format:
sk_satori_[random_string]
API keys are secrets that grant full access to your memory data. Never commit them to version control or expose them in client-side code.

Creating an API Key

1

Sign in to your dashboard

Visit satori.dev/dashboard and sign in with your account.
2

Navigate to API Keys

Click on “API Keys” in the sidebar navigation.
3

Create a new key

Click “Create API Key” and give it a descriptive name:
Production Key
Development Key
Testing Key
Use different keys for different environments (development, staging, production) to easily revoke access if needed.
4

Copy your key

Your API key will be displayed once. Copy it immediately and store it securely.
You won’t be able to see the key again after closing this dialog. If you lose it, you’ll need to create a new one.
5

Add to environment variables

Store your API key in your environment variables:
.env.local
SATORI_API_KEY=sk_satori_...
SATORI_URL=https://api.satori.dev

Using API Keys

Always use API keys on the server side, never in client-side code:
app/api/chat/route.ts
import { memoryTools } from '@satori/tools';

export async function POST(req: Request) {
  // ✅ Safe: Server-side API route
  const tools = memoryTools({
    apiKey: process.env.SATORI_API_KEY!,
    baseUrl: process.env.SATORI_URL!,
    userId: 'user-123',
  });
  
  // ... rest of your code
}

Client-Side (Never Do This)

Never expose your API key in client-side code:
// ❌ NEVER DO THIS
const tools = memoryTools({
  apiKey: 'sk_satori_...', // Exposed to users!
  baseUrl: 'https://api.satori.dev',
  userId: 'user-123',
});

Authentication Headers

When making direct HTTP requests to the Satori API, include your API key in the x-api-key header:
const response = await fetch('https://api.satori.dev/trpc/memory.search', {
  method: 'GET',
  headers: {
    'x-api-key': process.env.SATORI_API_KEY!,
    'Content-Type': 'application/json',
  },
});

Tenant Isolation Model

Satori uses a two-level isolation model:

Level 1: Tenant (API Key Owner)

Your API key identifies you as the tenant. All memories created with your key belong to your account.
// When you create an API key in the dashboard
const apiKey = await clerkClient.apiKeys.create({
  name: 'Production Key',
  subject: 'user_372Icb...', // Your Clerk user ID
});

Level 2: End Users (Your Application’s Users)

Within your tenant, you can have unlimited end users, each with isolated memories:
// Alice's memories
const aliceTools = memoryTools({
  apiKey: process.env.SATORI_API_KEY!,
  baseUrl: process.env.SATORI_URL!,
  userId: 'alice', // Your app's user identifier
});

// Bob's memories (completely separate)
const bobTools = memoryTools({
  apiKey: process.env.SATORI_API_KEY!,
  baseUrl: process.env.SATORI_URL!,
  userId: 'bob',
});
Think of it like this: Your API key is your “account”, and userId is how you separate your users’ data within your account.

API Key Verification Flow

Here’s what happens when you make a request:
1

Request includes API key

Your application sends a request with the x-api-key header:
headers: {
  'x-api-key': 'sk_satori_...'
}
2

Satori verifies with Clerk

The Satori server verifies the key with Clerk:
const verified = await clerkClient.apiKeys.verify(apiKey);
// verified.subject = "user_372Icb..." (your tenant ID)
3

Request is scoped to tenant

All database queries are automatically scoped to your tenant:
const memories = await db
  .select()
  .from(memories)
  .where(
    and(
      eq(memories.clerkUserId, 'user_372Icb...'), // Your tenant
      eq(memories.userId, 'alice') // Specific user
    )
  );

Managing API Keys

List Your Keys

View all your active API keys:
import { trpc } from '@/lib/trpc';

function APIKeysPage() {
  const { data: keys } = trpc.keys.list.useQuery();
  
  return (
    <div>
      {keys?.map((key) => (
        <div key={key.id}>
          <h3>{key.name}</h3>
          <p>Created: {key.createdAt}</p>
          <p>Last used: {key.lastUsedAt}</p>
        </div>
      ))}
    </div>
  );
}

Revoke a Key

Revoke an API key to immediately prevent all access:
const revokeKey = trpc.keys.revoke.useMutation();

await revokeKey.mutateAsync({ id: 'key-uuid' });
Revoking a key immediately stops all applications using that key. Make sure to update your applications with a new key before revoking the old one.

Security Best Practices

Store API keys in environment variables, never hardcode them:
// ✅ Good
apiKey: process.env.SATORI_API_KEY!

// ❌ Bad
apiKey: 'sk_satori_...'
Create a new API key and update your applications, then revoke the old key:
  1. Create new key in dashboard
  2. Update environment variables in all environments
  3. Deploy updates
  4. Revoke old key
Separate keys for development, staging, and production:
# .env.development
SATORI_API_KEY=sk_satori_dev_...

# .env.production
SATORI_API_KEY=sk_satori_prod_...
Check the “Last used” timestamp in your dashboard to detect unused or compromised keys.
Add environment files to .gitignore:
.gitignore
.env.local
.env*.local
.env.development.local
.env.production.local

Rate Limiting

API keys are subject to rate limits to prevent abuse:
Limit TypeValue
Requests per minute100
Memories per userUnlimited
Concurrent requests10
Need higher limits? Contact support to discuss enterprise plans.

Error Handling

Handle authentication errors gracefully:
try {
  const memories = await client.searchMemories('query');
} catch (error) {
  if (error.message.includes('Unauthorized')) {
    console.error('Invalid API key');
    // Notify admin, rotate key, etc.
  } else if (error.message.includes('rate limit')) {
    console.error('Rate limit exceeded');
    // Implement backoff strategy
  }
}

Next Steps