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
Sign in to your dashboard
Navigate to API Keys
Click on “API Keys” in the sidebar navigation.
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.
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.
Add to environment variables
Store your API key in your environment variables: SATORI_API_KEY = sk_satori_...
SATORI_URL = https://api.usesatori.sh
Using API Keys
Server-Side Usage (Recommended)
Always use API keys on the server side, never in client-side code:
import { memoryTools } from '@usesatori/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.usesatori.sh' ,
userId: 'user-123' ,
});
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.usesatori.sh/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:
Request includes API key
Your application sends a request with the x-api-key header: headers : {
'x-api-key' : 'sk_satori_...'
}
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)
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
Use environment variables
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:
Create new key in dashboard
Update environment variables in all environments
Deploy updates
Revoke old key
Use different keys per environment
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: .env.local
.env*.local
.env.development.local
.env.production.local
Rate Limiting
API keys are subject to rate limits to prevent abuse:
Limit Type Value Requests per minute 100 Memories per user Unlimited Concurrent requests 10
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