Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-Tenancy

Multi-tenancy is a core architectural pattern that allows a single SCIM Server instance to serve multiple organizations while keeping their data completely isolated. This chapter explains how SCIM Server implements multi-tenancy and how to use it effectively.

What is Multi-Tenancy?

Multi-tenancy is a software architecture where a single instance of an application serves multiple customers (tenants). Each tenant's data is isolated and invisible to other tenants, creating the appearance of having their own dedicated instance.

Benefits of Multi-Tenancy

  • Cost Efficiency: Shared infrastructure reduces operational costs
  • Simplified Management: Single deployment to maintain and update
  • Resource Optimization: Better utilization of hardware and services
  • Faster Scaling: Add new tenants without new infrastructure
  • Centralized Security: Consistent security policies across all tenants

SCIM Server's Multi-Tenant Approach

SCIM Server implements multi-tenancy at the application layer, providing:

  • Complete Data Isolation: No tenant can access another's data
  • Tenant-Specific Configuration: Schemas and settings per tenant
  • Performance Isolation: Resource quotas and monitoring per tenant
  • Independent Scaling: Different tenants can have different performance profiles

Core Concepts

Tenant Identification

Each tenant is identified by a unique tenant ID that's used throughout the system:

#![allow(unused)]
fn main() {
use scim_server::{ScimServer, TenantId, storage::InMemoryStorage};

// Create a server with multi-tenant support
let storage = InMemoryStorage::new();
let server = ScimServer::new(storage).await?;

// Operations are scoped to specific tenants
let tenant_id = TenantId::new("acme-corp");
let user = server.create_user(&tenant_id, user_data).await?;
}

Tenant Isolation

Data isolation is enforced at multiple levels:

  1. API Level: All operations require a tenant context
  2. Validation Level: Schemas are tenant-specific
  3. Storage Level: Data is partitioned by tenant
  4. Authentication Level: Users belong to specific tenants

Tenant Configuration

Each tenant can have its own configuration:

#![allow(unused)]
fn main() {
use scim_server::{TenantConfig, SchemaConfig};

let tenant_config = TenantConfig::builder()
    .tenant_id("acme-corp")
    .display_name("Acme Corporation")
    .max_users(1000)
    .custom_schema(custom_employee_schema)
    .features(vec!["bulk_operations", "filtering"])
    .build();

server.configure_tenant(tenant_config).await?;
}

Implementation Patterns

Basic Multi-Tenant Setup

The simplest multi-tenant setup uses tenant IDs in all operations:

use scim_server::{ScimServer, TenantId, storage::InMemoryStorage};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let storage = InMemoryStorage::new();
    let server = ScimServer::new(storage).await?;
    
    // Create users for different tenants
    let acme_tenant = TenantId::new("acme-corp");
    let beta_tenant = TenantId::new("beta-inc");
    
    // Acme Corp user
    let acme_user = server.create_user(&acme_tenant, json!({
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        "userName": "alice@acme.com",
        "displayName": "Alice Smith"
    })).await?;
    
    // Beta Inc user
    let beta_user = server.create_user(&beta_tenant, json!({
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        "userName": "bob@beta.com",
        "displayName": "Bob Johnson"
    })).await?;
    
    // Users are completely isolated
    let acme_users = server.list_users(&acme_tenant).await?;
    let beta_users = server.list_users(&beta_tenant).await?;
    
    // acme_users contains only Alice, beta_users contains only Bob
    
    Ok(())
}

HTTP Integration with Tenant Context

When integrating with HTTP frameworks, extract tenant ID from the request:

#![allow(unused)]
fn main() {
use axum::{extract::Path, Extension, Json};
use scim_server::{ScimServer, TenantId};

async fn create_user(
    Path(tenant_id): Path<String>,
    Extension(server): Extension<ScimServer>,
    Json(user_data): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, AppError> {
    let tenant = TenantId::new(tenant_id);
    let user = server.create_user(&tenant, user_data).await?;
    Ok(Json(user))
}

// Route: POST /tenants/{tenant_id}/scim/v2/Users
}

Tenant-Specific Schemas

Different tenants can have different schema requirements:

#![allow(unused)]
fn main() {
use scim_server::{CustomSchema, AttributeType, TenantId};

// Healthcare tenant needs additional fields
let healthcare_schema = CustomSchema::builder()
    .id("urn:healthcare:schemas:Employee")
    .add_attribute("licenseNumber", AttributeType::String, false)
    .add_attribute("department", AttributeType::String, false)
    .add_attribute("certifications", AttributeType::MultiValue, false)
    .build();

let healthcare_tenant = TenantId::new("healthcare-corp");
server.register_schema(&healthcare_tenant, healthcare_schema).await?;

// Financial tenant needs different fields
let financial_schema = CustomSchema::builder()
    .id("urn:financial:schemas:Employee")
    .add_attribute("employeeId", AttributeType::String, true)
    .add_attribute("clearanceLevel", AttributeType::String, false)
    .add_attribute("tradingPermissions", AttributeType::MultiValue, false)
    .build();

let financial_tenant = TenantId::new("financial-corp");
server.register_schema(&financial_tenant, financial_schema).await?;
}

Tenant Management

Creating and Configuring Tenants

#![allow(unused)]
fn main() {
use scim_server::{TenantConfig, ResourceQuota, TenantFeatures};

async fn setup_new_tenant(
    server: &ScimServer,
    tenant_id: &str,
    org_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let tenant = TenantId::new(tenant_id);
    
    // Configure tenant with specific limits and features
    let config = TenantConfig::builder()
        .tenant_id(tenant_id)
        .display_name(org_name)
        .quota(ResourceQuota {
            max_users: 500,
            max_groups: 50,
            max_custom_resources: 100,
        })
        .features(TenantFeatures {
            bulk_operations: true,
            patch_operations: true,
            filtering: true,
            sorting: true,
            ai_integration: false, // Disabled for basic plan
        })
        .build();
    
    server.create_tenant(config).await?;
    
    // Set up default schemas
    server.register_default_schemas(&tenant).await?;
    
    Ok(())
}
}

Tenant Discovery and Listing

#![allow(unused)]
fn main() {
// List all configured tenants
let tenants = server.list_tenants().await?;

for tenant in tenants {
    println!("Tenant: {} ({})", tenant.id, tenant.display_name);
    println!("  Users: {}/{}", tenant.user_count, tenant.max_users);
    println!("  Features: {:?}", tenant.enabled_features);
}

// Get specific tenant information
let tenant_info = server.get_tenant_info(&TenantId::new("acme-corp")).await?;
}

Storage Considerations

Database Multi-Tenancy

When using database storage, tenant isolation can be implemented in several ways:

Shared Database, Separate Schemas

-- Each tenant gets its own schema
CREATE SCHEMA tenant_acme_corp;
CREATE SCHEMA tenant_beta_inc;

-- Tables are created in tenant-specific schemas
CREATE TABLE tenant_acme_corp.users (...);
CREATE TABLE tenant_beta_inc.users (...);

Shared Database, Shared Tables with Tenant Column

-- Single table with tenant_id column
CREATE TABLE users (
    tenant_id VARCHAR(255) NOT NULL,
    user_id VARCHAR(255) NOT NULL,
    username VARCHAR(255) NOT NULL,
    -- ... other columns
    PRIMARY KEY (tenant_id, user_id)
);

-- All queries include tenant_id in WHERE clause
SELECT * FROM users WHERE tenant_id = 'acme-corp';

Separate Databases per Tenant

#![allow(unused)]
fn main() {
use scim_server::storage::DatabaseConfig;

// Configure separate databases per tenant
let acme_config = DatabaseConfig::new("postgresql://host/acme_corp_db");
let beta_config = DatabaseConfig::new("postgresql://host/beta_inc_db");

// Storage provider handles routing based on tenant
let storage = MultiTenantDatabaseStorage::new()
    .add_tenant("acme-corp", acme_config)
    .add_tenant("beta-inc", beta_config);
}

Security and Compliance

Authentication Integration

Combine multi-tenancy with authentication for complete security:

#![allow(unused)]
fn main() {
use scim_server::auth::{AuthenticationWitness, TenantAuthority};

async fn authenticated_operation(
    auth: AuthenticationWitness<Authenticated>,
    tenant_id: TenantId,
    server: &ScimServer,
) -> Result<(), AuthError> {
    // Verify user has access to this tenant
    let tenant_authority = auth.verify_tenant_access(&tenant_id)?;
    
    // Operation is now both authenticated and tenant-scoped
    let users = server.list_users_with_auth(
        &tenant_id,
        &auth,
        &tenant_authority
    ).await?;
    
    Ok(())
}
}

Audit Logging

Multi-tenant systems require comprehensive audit trails:

#![allow(unused)]
fn main() {
use scim_server::audit::{AuditEvent, AuditLogger};

// All operations are logged with tenant context
let audit_logger = AuditLogger::new();

// Log tenant-scoped operations
audit_logger.log(AuditEvent::UserCreated {
    tenant_id: "acme-corp",
    user_id: "user123",
    performed_by: "admin@acme.com",
    timestamp: Utc::now(),
}).await?;
}

Data Residency and Compliance

Different tenants may have different compliance requirements:

#![allow(unused)]
fn main() {
use scim_server::{DataResidency, ComplianceMode};

let eu_tenant_config = TenantConfig::builder()
    .tenant_id("eu-customer")
    .data_residency(DataResidency::EuropeanUnion)
    .compliance_mode(ComplianceMode::GDPR)
    .encryption_required(true)
    .audit_retention_days(2555) // 7 years
    .build();

let us_tenant_config = TenantConfig::builder()
    .tenant_id("us-customer")
    .data_residency(DataResidency::UnitedStates)
    .compliance_mode(ComplianceMode::SOX)
    .encryption_required(true)
    .audit_retention_days(2555)
    .build();
}

Performance and Scaling

Resource Isolation

Prevent one tenant from affecting others:

#![allow(unused)]
fn main() {
use scim_server::{ResourceLimits, RateLimiting};

let tenant_limits = ResourceLimits::builder()
    .max_requests_per_minute(1000)
    .max_concurrent_operations(50)
    .max_query_complexity(100)
    .memory_limit_mb(512)
    .build();

server.set_tenant_limits(&tenant_id, tenant_limits).await?;
}

Monitoring per Tenant

Track performance and usage metrics per tenant:

#![allow(unused)]
fn main() {
use scim_server::metrics::{TenantMetrics, MetricsCollector};

let metrics = server.get_tenant_metrics(&tenant_id).await?;

println!("Tenant {} metrics:", tenant_id);
println!("  Requests/minute: {}", metrics.requests_per_minute);
println!("  Average response time: {}ms", metrics.avg_response_time);
println!("  Storage usage: {}MB", metrics.storage_usage_mb);
println!("  Error rate: {}%", metrics.error_rate);
}

Best Practices

Tenant ID Strategy

Choose tenant IDs carefully:

#![allow(unused)]
fn main() {
// Good: Clear, unique, URL-safe
let tenant_id = TenantId::new("acme-corp-prod");

// Avoid: Ambiguous or containing sensitive data
let bad_tenant_id = TenantId::new("customer-123-secret-key");
}

Configuration Management

Use environment-specific configurations:

#![allow(unused)]
fn main() {
use scim_server::config::TenantConfigLoader;

// Load tenant configurations from external sources
let config_loader = TenantConfigLoader::new()
    .from_file("tenants.yaml")
    .from_env_prefix("SCIM_TENANT_")
    .from_database(&config_db);

let tenant_configs = config_loader.load_all().await?;
}

Error Handling

Provide tenant-aware error messages:

#![allow(unused)]
fn main() {
use scim_server::error::{ScimError, TenantError};

match server.get_user(&tenant_id, &user_id).await {
    Ok(user) => Ok(user),
    Err(ScimError::TenantNotFound(tenant)) => {
        Err(format!("Organization '{}' not found", tenant))
    },
    Err(ScimError::UserNotFound { tenant, user_id }) => {
        Err(format!("User '{}' not found in organization '{}'", user_id, tenant))
    },
    Err(e) => Err(e.into()),
}
}

Testing Multi-Tenant Code

Test with multiple tenants to ensure isolation:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_tenant_isolation() {
    let server = test_server().await;
    
    let tenant_a = TenantId::new("tenant-a");
    let tenant_b = TenantId::new("tenant-b");
    
    // Create user in tenant A
    let user_a = server.create_user(&tenant_a, user_data.clone()).await?;
    
    // Verify user is not visible in tenant B
    let tenant_b_users = server.list_users(&tenant_b).await?;
    assert!(tenant_b_users.is_empty());
    
    // Verify user is visible in tenant A
    let tenant_a_users = server.list_users(&tenant_a).await?;
    assert_eq!(tenant_a_users.len(), 1);
}
}

Multi-tenancy in SCIM Server provides a robust foundation for building SaaS applications that serve multiple organizations while maintaining security, performance, and compliance requirements.