Multi-Tenant Data Isolation

User Intent

"How do I isolate data for multiple tenants/users in my SaaS application?"

Operation

Concept: Per-user data isolation within a single Graphlit project SDK Method: createUser() + userId parameter in client initialization Use Case: SaaS applications with multiple users/tenants


Strategy: Per-User Scoping in Single Project

Graphlit supports multi-tenancy by creating users within your project and scoping SDK operations to each user.

import { Graphlit, Types } from 'graphlit-client';

// 1. Admin client (no user scoping) for user management
const adminClient = new Graphlit({
  organizationId: process.env.GRAPHLIT_ORGANIZATION_ID!,
  environmentId: process.env.GRAPHLIT_ENVIRONMENT_ID!,
  jwtSecret: process.env.GRAPHLIT_JWT_SECRET!,
});

// 2. When a user signs up (e.g., via Clerk, Supabase, Auth0)
async function onUserSignUp(authUser: { id: string; name: string; email: string }) {
  // Create user in Graphlit (maps to your auth provider's user)
  const user = await adminClient.createUser({
    name: authUser.name,
    identifier: authUser.id,  // Your auth provider's user ID (Clerk, Supabase, etc.)
    type: Types.UserTypes.Human,
  });
  
  // Store user.createUser.id in your database alongside auth user
  await db.users.update(authUser.id, {
    graphlitUserId: user.createUser.id
  });
}

// 3. On each user request, create scoped client
async function handleUserRequest(authUserId: string, request: any) {
  // Get Graphlit user ID from your database
  const graphlitUserId = await db.users.getGraphlitUserId(authUserId);
  
  // Create user-scoped client
  const userClient = new Graphlit({
    organizationId: process.env.GRAPHLIT_ORGANIZATION_ID!,
    environmentId: process.env.GRAPHLIT_ENVIRONMENT_ID!,
    jwtSecret: process.env.GRAPHLIT_JWT_SECRET!,
    userId: graphlitUserId,  // ← ALL operations now scoped to this user
  });
  
  // All content, conversations, and feeds are automatically isolated
  const results = await userClient.queryContents({
    filter: { search: request.query },
  });
  
  return results;  // Only returns this user's content
}

What Gets Scoped?

When you pass userId to the Graphlit client:

User-Scoped (Isolated per User):

  • Content - Documents, files, web pages ingested by this user

  • Conversations - Chat history and AI interactions

  • Feeds - Connected data sources (Slack, Gmail, etc.)

Project-Scoped (Shared Across Users):

  • 📋 Specifications - AI model configurations

  • 📋 Workflows - Content processing pipelines

This means you define AI models and workflows once at the project level, and each user gets isolated data automatically.


Complete Example: Multi-Tenant SaaS

import { Graphlit, Types } from 'graphlit-client';

export class GraphlitService {
  private adminClient: Graphlit;
  
  constructor() {
    // Admin client for user management
    this.adminClient = new Graphlit({
      organizationId: process.env.GRAPHLIT_ORGANIZATION_ID!,
      environmentId: process.env.GRAPHLIT_ENVIRONMENT_ID!,
      jwtSecret: process.env.GRAPHLIT_JWT_SECRET!,
    });
  }
  
  // Create user on signup
  async createUser(authUser: { id: string; name: string; email: string }) {
    // Check if user already exists
    try {
      const existing = await this.adminClient.getUserByIdentifier(authUser.id);
      if (existing.userByIdentifier) {
        return existing.userByIdentifier.id;
      }
    } catch (error) {
      // User doesn't exist, create new one
    }
    
    const user = await this.adminClient.createUser({
      name: authUser.name,
      identifier: authUser.id,  // Clerk/Supabase/Auth0 user ID
      type: Types.UserTypes.Human,
    });
    
    return user.createUser.id;
  }
  
  // Get user-scoped client
  getUserClient(graphlitUserId: string): Graphlit {
    return new Graphlit({
      organizationId: process.env.GRAPHLIT_ORGANIZATION_ID!,
      environmentId: process.env.GRAPHLIT_ENVIRONMENT_ID!,
      jwtSecret: process.env.GRAPHLIT_JWT_SECRET!,
      userId: graphlitUserId,
    });
  }
}

// Usage in your API routes
const graphlitService = new GraphlitService();

// POST /api/signup
async function handleSignup(req: Request) {
  const { clerkUserId, name, email } = req.body;
  
  // Create Graphlit user
  const graphlitUserId = await graphlitService.createUser({
    id: clerkUserId,
    name,
    email,
  });
  
  // Store in your database
  await db.users.create({
    clerkUserId,
    graphlitUserId,
    name,
    email,
  });
}

// POST /api/documents/upload
async function handleUpload(req: Request) {
  const { userId } = req.auth;  // From your auth middleware
  const { documentUrl } = req.body;
  
  // Get user's Graphlit client
  const dbUser = await db.users.findByClerkId(userId);
  const client = graphlitService.getUserClient(dbUser.graphlitUserId);
  
  // Upload document (automatically scoped to this user)
  const content = await client.ingestUri(
    documentUrl,
    'User Document',
    undefined,
    undefined,
    true
  );
  
  return { contentId: content.ingestUri.id };
}

// POST /api/chat
async function handleChat(req: Request) {
  const { userId } = req.auth;
  const { message, conversationId } = req.body;
  
  const dbUser = await db.users.findByClerkId(userId);
  const client = graphlitService.getUserClient(dbUser.graphlitUserId);
  
  // Chat only accesses this user's content
  const response = await client.promptConversation(
    message,
    conversationId
  );
  
  return { reply: response.promptConversation.message?.message };
}

Multi-Tenant Benefits

Complete Data Isolation: Each user's content/conversations/feeds are isolated Single Project Management: One project, shared specifications/workflows Scalable: Thousands of users per project Auth Integration: Works with Clerk, Supabase, Auth0, custom auth Usage Tracking: Per-user usage tracking available


Implementation Checklist


Alternative: Legacy ownerId (Deprecated)

The legacy ownerId parameter is still supported but deprecated. Use userId for new applications.


Last updated

Was this helpful?