Claude Code Security Patterns for Multi-Tenant SaaS Applications

Overview

Building secure multi-tenant SaaS applications with Claude Code requires careful architectural decisions around data isolation, authentication, authorization, and compliance. This guide provides comprehensive patterns for implementing tenant isolation, securing AI workloads, and maintaining compliance in production environments.

Multi-Tenant Architecture Models

1. Silo Model - Complete Isolation

interface SiloArchitecture {
  // Each tenant gets dedicated resources
  tenant: {
    database: Database;
    claudeApiKey: string;
    computeInstance: ComputeResource;
    storageAccount: StorageAccount;
  };
}
 
class SiloTenantManager {
  async provisionTenant(tenantId: string): Promise<TenantResources> {
    // Create dedicated database
    const database = await this.createDatabase(`db_${tenantId}`);
    
    // Generate tenant-specific Claude API key
    const apiKey = await this.generateTenantApiKey(tenantId);
    
    // Provision isolated compute
    const compute = await this.provisionCompute({
      tenantId,
      isolation: 'dedicated',
      networkPolicy: 'isolated'
    });
    
    return {
      database,
      apiKey,
      compute,
      isolation: 'silo'
    };
  }
}

2. Pool Model - Shared Resources

class PooledTenantManager {
  private tenantContext: Map<string, TenantContext> = new Map();
 
  async executeWithTenantContext<T>(
    tenantId: string,
    operation: (context: TenantContext) => Promise<T>
  ): Promise<T> {
    const context = await this.getTenantContext(tenantId);
    
    // Inject tenant context into all operations
    return await this.withTenantIsolation(context, operation);
  }
 
  private async withTenantIsolation<T>(
    context: TenantContext,
    operation: (context: TenantContext) => Promise<T>
  ): Promise<T> {
    // Set tenant context in async local storage
    AsyncLocalStorage.run(context, async () => {
      // Apply row-level security
      await this.applyRowLevelSecurity(context.tenantId);
      
      // Execute operation with tenant context
      return await operation(context);
    });
  }
}

3. Bridge Model - Hybrid Approach

class BridgeModelArchitecture {
  async determineTenantIsolation(tenant: Tenant): Promise<IsolationStrategy> {
    // Premium tenants get dedicated resources
    if (tenant.tier === 'enterprise') {
      return {
        model: 'silo',
        database: 'dedicated',
        compute: 'dedicated',
        claudeApi: 'dedicated-key'
      };
    }
    
    // Standard tenants share resources with isolation
    return {
      model: 'pool',
      database: 'shared-with-rls',
      compute: 'shared-with-quotas',
      claudeApi: 'shared-with-rate-limits'
    };
  }
}

Data Isolation Strategies

1. Database-Per-Tenant Isolation

class DatabasePerTenantStrategy {
  private connectionPools: Map<string, DatabasePool> = new Map();
 
  async getConnection(tenantId: string): Promise<DatabaseConnection> {
    if (!this.connectionPools.has(tenantId)) {
      const pool = await this.createTenantPool(tenantId);
      this.connectionPools.set(tenantId, pool);
    }
    
    return this.connectionPools.get(tenantId)!.getConnection();
  }
 
  private async createTenantPool(tenantId: string): Promise<DatabasePool> {
    const dbName = `tenant_${tenantId}_db`;
    
    return new DatabasePool({
      host: process.env.DB_HOST,
      database: dbName,
      // Tenant-specific credentials
      user: `tenant_${tenantId}_user`,
      password: await this.getSecretForTenant(tenantId),
      // Connection pool settings
      max: 20,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    });
  }
}

2. Schema-Per-Tenant with Row-Level Security

class SchemaPerTenantStrategy {
  async setupTenantSchema(tenantId: string): Promise<void> {
    const schemaName = `tenant_${tenantId}`;
    
    await this.db.query(`
      CREATE SCHEMA IF NOT EXISTS ${schemaName};
      
      -- Create tenant-specific user
      CREATE USER ${schemaName}_user WITH PASSWORD '${await this.generatePassword()}';
      
      -- Grant schema access
      GRANT USAGE ON SCHEMA ${schemaName} TO ${schemaName}_user;
      GRANT CREATE ON SCHEMA ${schemaName} TO ${schemaName}_user;
      
      -- Set default schema for user
      ALTER USER ${schemaName}_user SET search_path TO ${schemaName};
    `);
    
    // Apply row-level security policies
    await this.applyRLSPolicies(schemaName, tenantId);
  }
 
  private async applyRLSPolicies(schema: string, tenantId: string): Promise<void> {
    // Enable RLS on all tables
    const tables = await this.getTablesInSchema(schema);
    
    for (const table of tables) {
      await this.db.query(`
        ALTER TABLE ${schema}.${table} ENABLE ROW LEVEL SECURITY;
        
        -- Create policy for tenant isolation
        CREATE POLICY tenant_isolation_policy ON ${schema}.${table}
          FOR ALL
          USING (tenant_id = current_setting('app.current_tenant')::uuid);
      `);
    }
  }
}

3. Shared Database with Tenant Context

class SharedDatabaseStrategy {
  private tenantContext: AsyncLocalStorage<TenantContext>;
 
  constructor() {
    this.tenantContext = new AsyncLocalStorage();
  }
 
  async executeQuery<T>(
    query: string,
    params: any[],
    tenantId: string
  ): Promise<T> {
    return this.tenantContext.run({ tenantId }, async () => {
      // Automatically inject tenant_id into all queries
      const modifiedQuery = this.injectTenantFilter(query, tenantId);
      
      // Add tenant_id to query parameters
      const tenantParams = [...params, tenantId];
      
      // Execute with tenant context
      return await this.db.query(modifiedQuery, tenantParams);
    });
  }
 
  private injectTenantFilter(query: string, tenantId: string): string {
    // Parse and modify query to include tenant filter
    const ast = parseSQL(query);
    
    // Add WHERE tenant_id = ? to all SELECT/UPDATE/DELETE
    if (ast.type === 'select' || ast.type === 'update' || ast.type === 'delete') {
      ast.where = ast.where 
        ? { type: 'and', left: ast.where, right: { type: 'eq', left: 'tenant_id', right: '?' } }
        : { type: 'eq', left: 'tenant_id', right: '?' };
    }
    
    return generateSQL(ast);
  }
}

Claude Code Security Patterns

1. Tenant-Specific API Key Management

class TenantClaudeAPIManager {
  private keyVault: KeyVault;
  private rateLimiters: Map<string, RateLimiter> = new Map();
 
  async getAPIKeyForTenant(tenantId: string): Promise<string> {
    // Retrieve encrypted API key from vault
    const encryptedKey = await this.keyVault.getSecret(`claude-api-${tenantId}`);
    
    // Decrypt with tenant-specific key
    const decryptionKey = await this.getTenantDecryptionKey(tenantId);
    return this.decrypt(encryptedKey, decryptionKey);
  }
 
  async executeClaudeRequest(
    tenantId: string,
    request: ClaudeRequest
  ): Promise<ClaudeResponse> {
    // Get tenant-specific API key
    const apiKey = await this.getAPIKeyForTenant(tenantId);
    
    // Apply tenant-specific rate limiting
    const rateLimiter = this.getRateLimiterForTenant(tenantId);
    await rateLimiter.checkLimit();
    
    // Add tenant context to request
    const enrichedRequest = {
      ...request,
      headers: {
        ...request.headers,
        'X-Tenant-ID': tenantId,
        'X-API-Key': apiKey
      },
      metadata: {
        tenantId,
        timestamp: Date.now(),
        requestId: generateRequestId()
      }
    };
    
    // Execute with audit logging
    return await this.executeWithAudit(enrichedRequest);
  }
}

2. Secure Prompt Isolation

class SecurePromptManager {
  async buildTenantPrompt(
    tenantId: string,
    userPrompt: string,
    context?: TenantContext
  ): Promise<SecurePrompt> {
    // Validate and sanitize user input
    const sanitizedPrompt = await this.sanitizePrompt(userPrompt);
    
    // Get tenant-specific system prompt
    const systemPrompt = await this.getTenantSystemPrompt(tenantId);
    
    // Build secure prompt with isolation
    return {
      messages: [
        {
          role: 'system',
          content: `${systemPrompt}\n\nTenant ID: ${tenantId}\nData Isolation: ENFORCED`
        },
        {
          role: 'user',
          content: sanitizedPrompt
        }
      ],
      security: {
        tenantId,
        dataAccessScope: context?.allowedResources || [],
        prohibitedActions: ['cross-tenant-access', 'system-modification'],
        maxTokens: this.getTenantTokenLimit(tenantId)
      }
    };
  }
 
  private async sanitizePrompt(prompt: string): Promise<string> {
    // Remove potential injection attacks
    const sanitized = prompt
      .replace(/\{\{.*?\}\}/g, '') // Remove template injections
      .replace(/system:/gi, '')     // Remove system role attempts
      .replace(/ignore previous/gi, ''); // Remove override attempts
    
    // Validate against known attack patterns
    if (await this.detectInjectionAttempt(sanitized)) {
      throw new SecurityError('Potential injection detected');
    }
    
    return sanitized;
  }
}

3. Tenant Context Propagation

class TenantContextPropagator {
  private contextStorage: AsyncLocalStorage<TenantContext>;
 
  async propagateContext<T>(
    tenantId: string,
    operation: () => Promise<T>
  ): Promise<T> {
    const context: TenantContext = {
      tenantId,
      userId: getCurrentUserId(),
      permissions: await this.getUserPermissions(),
      dataScope: await this.getTenantDataScope(tenantId),
      auditInfo: {
        requestId: generateRequestId(),
        timestamp: Date.now(),
        ip: getClientIP()
      }
    };
 
    return this.contextStorage.run(context, async () => {
      // Set context in all downstream services
      await this.setDatabaseContext(context);
      await this.setClaudeContext(context);
      await this.setCacheContext(context);
      
      // Execute operation with full context
      return await operation();
    });
  }
 
  getCurrentContext(): TenantContext {
    const context = this.contextStorage.getStore();
    if (!context) {
      throw new Error('No tenant context available');
    }
    return context;
  }
}

Authentication & Authorization

1. Multi-Tenant OAuth Implementation

class MultiTenantOAuth {
  async authenticate(request: AuthRequest): Promise<AuthResult> {
    // Extract tenant from subdomain or path
    const tenantId = this.extractTenantId(request);
    
    // Get tenant-specific OAuth config
    const oauthConfig = await this.getTenantOAuthConfig(tenantId);
    
    // Validate OAuth token with tenant context
    const token = await this.validateToken(request.token, oauthConfig);
    
    // Create tenant-scoped session
    return {
      tenantId,
      userId: token.sub,
      permissions: await this.getTenantPermissions(tenantId, token.sub),
      sessionId: generateSessionId(),
      expiresAt: token.exp
    };
  }
 
  private extractTenantId(request: AuthRequest): string {
    // Option 1: Subdomain (tenant1.app.com)
    const subdomain = request.hostname.split('.')[0];
    if (subdomain !== 'www' && subdomain !== 'app') {
      return subdomain;
    }
    
    // Option 2: Path prefix (/tenant/tenant1)
    const pathMatch = request.path.match(/^\/tenant\/([^\/]+)/);
    if (pathMatch) {
      return pathMatch[1];
    }
    
    // Option 3: Custom header
    if (request.headers['x-tenant-id']) {
      return request.headers['x-tenant-id'];
    }
    
    throw new Error('Unable to determine tenant');
  }
}

2. Role-Based Access Control (RBAC)

class TenantRBACManager {
  async checkPermission(
    tenantId: string,
    userId: string,
    resource: string,
    action: string
  ): Promise<boolean> {
    // Get user's roles within tenant
    const roles = await this.getUserTenantRoles(tenantId, userId);
    
    // Check if any role grants the permission
    for (const role of roles) {
      const permissions = await this.getRolePermissions(tenantId, role);
      
      if (this.hasPermission(permissions, resource, action)) {
        // Log successful authorization
        await this.auditLog.record({
          tenantId,
          userId,
          resource,
          action,
          result: 'granted',
          timestamp: Date.now()
        });
        
        return true;
      }
    }
    
    // Log denied authorization
    await this.auditLog.record({
      tenantId,
      userId,
      resource,
      action,
      result: 'denied',
      timestamp: Date.now()
    });
    
    return false;
  }
 
  async createTenantRole(
    tenantId: string,
    role: TenantRole
  ): Promise<void> {
    // Ensure role is scoped to tenant
    const scopedRole = {
      ...role,
      tenantId,
      permissions: role.permissions.map(p => ({
        ...p,
        tenantId,
        scope: 'tenant'
      }))
    };
    
    await this.db.insert('tenant_roles', scopedRole);
  }
}

Compliance & Audit

1. Comprehensive Audit Logging

class TenantAuditLogger {
  private auditStorage: AuditStorage;
  private encryptionKey: string;
 
  async logClaudeInteraction(
    tenantId: string,
    interaction: ClaudeInteraction
  ): Promise<void> {
    const auditEntry: AuditEntry = {
      id: generateAuditId(),
      tenantId,
      userId: interaction.userId,
      timestamp: Date.now(),
      action: 'claude_api_call',
      resource: 'claude_code',
      details: {
        model: interaction.model,
        tokenCount: interaction.tokenCount,
        cost: interaction.estimatedCost,
        // Don't log actual prompts/responses for privacy
        promptHash: this.hashContent(interaction.prompt),
        responseHash: this.hashContent(interaction.response),
        toolsUsed: interaction.toolsUsed,
        duration: interaction.duration
      },
      compliance: {
        dataClassification: interaction.dataClassification,
        retentionPolicy: this.getRetentionPolicy(tenantId),
        encryption: 'AES-256-GCM'
      }
    };
 
    // Encrypt sensitive data
    const encryptedEntry = await this.encryptAuditEntry(auditEntry);
    
    // Store with tenant partitioning
    await this.auditStorage.store(tenantId, encryptedEntry);
    
    // Real-time compliance monitoring
    await this.checkComplianceRules(auditEntry);
  }
 
  private async checkComplianceRules(entry: AuditEntry): Promise<void> {
    // Check for suspicious patterns
    if (entry.details.tokenCount > 100000) {
      await this.alertCompliance('high_token_usage', entry);
    }
    
    // Check for cross-tenant access attempts
    if (await this.detectCrossTenantAccess(entry)) {
      await this.alertSecurity('cross_tenant_attempt', entry);
    }
  }
}

2. Data Residency & Sovereignty

class DataResidencyManager {
  async routeClaudeRequest(
    tenantId: string,
    request: ClaudeRequest
  ): Promise<ClaudeEndpoint> {
    // Get tenant's data residency requirements
    const residency = await this.getTenantResidency(tenantId);
    
    // Select appropriate Claude endpoint
    const endpoint = this.selectEndpoint(residency);
    
    // Ensure data doesn't leave required region
    if (!this.isCompliantEndpoint(endpoint, residency)) {
      throw new ComplianceError(
        `Cannot process request: endpoint ${endpoint.region} violates residency requirement ${residency.region}`
      );
    }
    
    return endpoint;
  }
 
  private selectEndpoint(residency: DataResidency): ClaudeEndpoint {
    const endpoints = {
      'us': { url: 'https://api.anthropic.com', region: 'us-east-1' },
      'eu': { url: 'https://eu.api.anthropic.com', region: 'eu-west-1' },
      'apac': { url: 'https://apac.api.anthropic.com', region: 'ap-southeast-1' }
    };
    
    return endpoints[residency.region] || endpoints['us'];
  }
}

3. GDPR & Privacy Compliance

class GDPRComplianceManager {
  async handleDataRequest(
    tenantId: string,
    userId: string,
    requestType: 'access' | 'deletion' | 'portability'
  ): Promise<ComplianceResponse> {
    switch (requestType) {
      case 'access':
        return await this.handleAccessRequest(tenantId, userId);
      
      case 'deletion':
        return await this.handleDeletionRequest(tenantId, userId);
      
      case 'portability':
        return await this.handlePortabilityRequest(tenantId, userId);
    }
  }
 
  private async handleDeletionRequest(
    tenantId: string,
    userId: string
  ): Promise<ComplianceResponse> {
    // Delete user's Claude interaction history
    await this.deleteClaudeHistory(tenantId, userId);
    
    // Delete cached responses
    await this.deleteCachedData(tenantId, userId);
    
    // Delete from vector stores
    await this.deleteVectorEmbeddings(tenantId, userId);
    
    // Audit the deletion
    await this.auditDeletion(tenantId, userId);
    
    return {
      status: 'completed',
      deletedRecords: await this.getDeletedCount(),
      timestamp: Date.now(),
      certificate: await this.generateDeletionCertificate()
    };
  }
}

Security Best Practices

1. Zero-Trust Architecture

class ZeroTrustSecurityManager {
  async validateRequest(request: SecureRequest): Promise<ValidationResult> {
    // Never trust, always verify
    const validations = await Promise.all([
      this.validateAuthentication(request),
      this.validateAuthorization(request),
      this.validateTenantContext(request),
      this.validateDataAccess(request),
      this.validateNetworkOrigin(request)
    ]);
    
    // All validations must pass
    const allValid = validations.every(v => v.valid);
    
    if (!allValid) {
      const failures = validations
        .filter(v => !v.valid)
        .map(v => v.reason);
      
      throw new SecurityError(`Request validation failed: ${failures.join(', ')}`);
    }
    
    return { valid: true, context: this.buildSecurityContext(validations) };
  }
}

2. Encryption at Rest and in Transit

class EncryptionManager {
  async encryptTenantData(
    tenantId: string,
    data: any
  ): Promise<EncryptedData> {
    // Get tenant-specific encryption key
    const tenantKey = await this.getTenantEncryptionKey(tenantId);
    
    // Encrypt with AES-256-GCM
    const encrypted = await crypto.subtle.encrypt(
      {
        name: 'AES-GCM',
        iv: crypto.getRandomValues(new Uint8Array(12)),
        tagLength: 128
      },
      tenantKey,
      new TextEncoder().encode(JSON.stringify(data))
    );
    
    return {
      ciphertext: encrypted,
      algorithm: 'AES-256-GCM',
      keyId: await this.getKeyId(tenantKey),
      tenantId
    };
  }
}

3. Security Monitoring & Threat Detection

class ThreatDetectionSystem {
  async monitorClaudeUsage(
    tenantId: string,
    usage: ClaudeUsageMetrics
  ): Promise<ThreatAssessment> {
    const anomalies = await this.detectAnomalies(tenantId, usage);
    
    if (anomalies.length > 0) {
      // Analyze threat level
      const threatLevel = this.assessThreatLevel(anomalies);
      
      if (threatLevel === 'high') {
        // Immediate action
        await this.blockTenant(tenantId);
        await this.alertSecurityTeam(tenantId, anomalies);
      } else if (threatLevel === 'medium') {
        // Rate limit and monitor
        await this.applyStrictRateLimits(tenantId);
        await this.enableEnhancedMonitoring(tenantId);
      }
      
      return {
        tenantId,
        threatLevel,
        anomalies,
        recommendedActions: this.getRecommendedActions(threatLevel)
      };
    }
    
    return { tenantId, threatLevel: 'low', anomalies: [] };
  }
 
  private async detectAnomalies(
    tenantId: string,
    usage: ClaudeUsageMetrics
  ): Promise<Anomaly[]> {
    const anomalies: Anomaly[] = [];
    
    // Check for unusual token usage
    if (usage.tokensPerMinute > await this.getBaselineTPM(tenantId) * 10) {
      anomalies.push({
        type: 'excessive_token_usage',
        severity: 'high',
        details: `TPM: ${usage.tokensPerMinute}`
      });
    }
    
    // Check for suspicious prompts
    if (usage.suspiciousPromptCount > 0) {
      anomalies.push({
        type: 'suspicious_prompts',
        severity: 'medium',
        details: `Count: ${usage.suspiciousPromptCount}`
      });
    }
    
    // Check for cross-tenant access attempts
    if (usage.crossTenantAttempts > 0) {
      anomalies.push({
        type: 'cross_tenant_access',
        severity: 'critical',
        details: `Attempts: ${usage.crossTenantAttempts}`
      });
    }
    
    return anomalies;
  }
}

Implementation Checklist

Security Requirements

  • Implement tenant isolation at database level
  • Set up per-tenant API key management
  • Configure row-level security policies
  • Implement comprehensive audit logging
  • Set up encryption for data at rest
  • Configure TLS for all communications
  • Implement zero-trust validation

Compliance Requirements

  • GDPR data handling procedures
  • Data residency configuration
  • Audit trail retention policies
  • Privacy-preserving logging
  • Right to deletion implementation
  • Data portability APIs
  • Compliance reporting dashboards

Monitoring Requirements

  • Real-time security monitoring
  • Anomaly detection system
  • Usage analytics per tenant
  • Cost tracking and allocation
  • Performance monitoring
  • Threat detection alerts
  • Compliance violation alerts

Conclusion

Implementing secure multi-tenant SaaS applications with Claude Code requires careful attention to data isolation, authentication, and compliance. By following these patterns and best practices, you can build systems that maintain strong security boundaries while delivering powerful AI capabilities to each tenant.

Last verified: 2025-07-21
Source: Industry best practices and security research