Skip to main content

Overview

Satori implements a robust multi-tenant architecture that ensures complete data isolation between tenants and their end users. This page explains how isolation works and why it matters for your application.

Two-Level Isolation Model

Satori uses a hierarchical isolation model with two levels:

Level 1: Tenant Isolation (clerkUserId)

Each API key is tied to a specific Clerk user ID (the tenant). All memories created with that key belong to that tenant’s account.
// Your API key → Your Clerk user ID
const apiKey = 'sk_satori_abc...';
// This key is bound to clerkUserId: "user_372Icb..."
Tenants are completely isolated from each other. Developer A can never access Developer B’s data, even if they know the user IDs.

Level 2: User Isolation (userId)

Within your tenant account, each userId you provide gets isolated memory storage:
// Alice's memories
const aliceMemories = await client.searchMemories('preferences');
// Only searches memories where userId = 'alice'

// Bob's memories (completely separate)
const bobMemories = await client.searchMemories('preferences');
// Only searches memories where userId = 'bob'

Database Schema

Here’s how isolation is enforced at the database level:
CREATE TABLE memories (
  id UUID PRIMARY KEY,
  clerk_user_id TEXT NOT NULL,  -- Tenant identifier
  user_id TEXT NOT NULL,         -- End-user identifier
  content TEXT NOT NULL,
  embedding VECTOR(1536),
  metadata JSONB,
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);

-- Composite index for fast tenant + user queries
CREATE INDEX memories_tenant_user_idx 
ON memories (clerk_user_id, user_id);
Every query automatically includes both identifiers:
// When you search memories
const memories = await db
  .select()
  .from(memories)
  .where(
    and(
      eq(memories.clerkUserId, 'user_372Icb...'), // Your tenant
      eq(memories.userId, 'alice')                 // Specific user
    )
  );
It’s impossible to query memories without both identifiers, ensuring complete isolation.

Isolation Guarantees

Tenant Isolation

Guarantee: Tenants can never access each other’s dataEnforced by:
  • API key verification
  • Database-level filtering
  • Automatic query scoping

User Isolation

Guarantee: Users within a tenant can never access each other’s memoriesEnforced by:
  • Required userId parameter
  • Composite database indexes
  • Query-level filtering

Cross-Tenant Protection

Guarantee: Even with a valid userId, cross-tenant access is impossibleEnforced by:
  • API key binding to clerkUserId
  • Middleware authentication
  • Database constraints

Data Encryption

Guarantee: Data is encrypted at rest and in transitEnforced by:
  • PostgreSQL encryption
  • TLS/HTTPS connections
  • Secure key storage

Practical Examples

Example 1: Multi-User Application

You’re building a chat application with 1000 users:
// User Alice logs in
const aliceSession = await auth.getSession();
const aliceUserId = aliceSession.userId; // "alice"

// Create memory tools for Alice
const aliceTools = memoryTools({
  apiKey: process.env.SATORI_API_KEY!, // Your tenant key
  baseUrl: process.env.SATORI_URL!,
  userId: aliceUserId, // Alice's ID
});

// User Bob logs in (different session)
const bobSession = await auth.getSession();
const bobUserId = bobSession.userId; // "bob"

// Create memory tools for Bob
const bobTools = memoryTools({
  apiKey: process.env.SATORI_API_KEY!, // Same tenant key
  baseUrl: process.env.SATORI_URL!,
  userId: bobUserId, // Bob's ID
});
Result: Alice and Bob’s memories are completely separate, even though they use the same API key (your tenant).

Example 2: Multi-Tenant SaaS

You’re building a SaaS where each company gets their own account:
// Company A's API key
const companyAKey = 'sk_satori_companyA...';

// Company A's users
const aliceTools = memoryTools({
  apiKey: companyAKey,
  baseUrl: process.env.SATORI_URL!,
  userId: 'alice',
});

// Company B's API key (different tenant)
const companyBKey = 'sk_satori_companyB...';

// Company B's users
const charlieTools = memoryTools({
  apiKey: companyBKey,
  baseUrl: process.env.SATORI_URL!,
  userId: 'charlie',
});
Result: Company A and Company B’s data is completely isolated at the tenant level, and their users are isolated within each tenant.
In a multi-tenant SaaS, each company should have their own API key. Don’t share API keys across companies.

User ID Best Practices

Use IDs from your authentication system (Auth0, Clerk, Firebase, etc.):
// ✅ Good: Stable user ID
userId: user.id // "user_2abc123..."

// ❌ Bad: Email (can change)
userId: user.email

// ❌ Bad: Username (can change)
userId: user.username
Each user must have a unique identifier:
// ❌ NEVER DO THIS
const tools = memoryTools({
  apiKey: process.env.SATORI_API_KEY!,
  baseUrl: process.env.SATORI_URL!,
  userId: 'shared-user', // All users share memories!
});
If you have multiple contexts per user, use namespaced IDs:
// Different workspaces for the same user
userId: `${user.id}:workspace:${workspaceId}`
// Example: "user_123:workspace:acme-corp"

// Different conversation threads
userId: `${user.id}:thread:${threadId}`
// Example: "user_123:thread:support-2024-01"
Keep a record of how you generate user IDs for consistency:
/**
 * User ID format: {authProvider}_{userId}
 * Examples:
 * - clerk_user_2abc123
 * - auth0_auth0|123456
 * - firebase_uid123abc
 */
function getSatoriUserId(user: User): string {
  return `${user.provider}_${user.id}`;
}

Security Considerations

API Key Security

Always use API keys on the server:
// ✅ Server-side API route
export async function POST(req: Request) {
  const tools = memoryTools({
    apiKey: process.env.SATORI_API_KEY!,
    // ... safe on server
  });
}

User ID Validation

Always validate user IDs before using them:
export async function POST(req: Request) {
  // Get authenticated user from your auth system
  const session = await getSession(req);
  
  if (!session?.userId) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  // Use the authenticated user's ID
  const tools = memoryTools({
    apiKey: process.env.SATORI_API_KEY!,
    baseUrl: process.env.SATORI_URL!,
    userId: session.userId, // Verified by your auth system
  });
}
Never trust user IDs from client requests. Always verify the user’s identity through your authentication system.

Testing Isolation

Verify isolation in your tests:
import { MemoryClient } from '@satori/tools';

describe('Memory Isolation', () => {
  const config = {
    apiKey: process.env.SATORI_API_KEY!,
    baseUrl: process.env.SATORI_URL!,
  };
  
  it('isolates memories between users', async () => {
    // Create clients for two users
    const aliceClient = new MemoryClient({ ...config, userId: 'alice' });
    const bobClient = new MemoryClient({ ...config, userId: 'bob' });
    
    // Alice saves a memory
    await aliceClient.addMemory('Alice likes TypeScript');
    
    // Bob searches for memories
    const bobMemories = await bobClient.searchMemories('TypeScript');
    
    // Bob should not see Alice's memory
    expect(bobMemories).toHaveLength(0);
  });
});

Compliance and Privacy

Satori’s isolation model helps you comply with privacy regulations:

GDPR Compliance

User data is isolated and can be deleted per user with deleteMemory()

Data Residency

Choose your deployment region to comply with data residency requirements

Right to be Forgotten

Delete all memories for a user to fulfill deletion requests

Data Portability

Export all memories for a user with getAllMemories()

Next Steps