@company-manager/docs

Tenant Isolation

Multi-tenant data isolation, context management, and cross-tenant protection diagrams

Tenant Isolation

This page covers the multi-tenant architecture ensuring complete data isolation between tenants.

Isolation Architecture

flowchart TB
    subgraph "Request Layer"
        REQ1[Tenant A Request]
        REQ2[Tenant B Request]
        REQ3[Tenant C Request]
    end

    subgraph "Context Resolution"
        CTX_RES[Context Resolver]
        TENANT_A_CTX[Tenant A Context]
        TENANT_B_CTX[Tenant B Context]
        TENANT_C_CTX[Tenant C Context]
    end

    subgraph "Data Access Layer"
        DAL[Data Access Layer]
        FILTER[Tenant Filter]
    end

    subgraph "Database"
        DB[(Shared Database)]
        DATA_A[Tenant A Data]
        DATA_B[Tenant B Data]
        DATA_C[Tenant C Data]
    end

    REQ1 --> CTX_RES
    REQ2 --> CTX_RES
    REQ3 --> CTX_RES

    CTX_RES --> TENANT_A_CTX
    CTX_RES --> TENANT_B_CTX
    CTX_RES --> TENANT_C_CTX

    TENANT_A_CTX --> DAL
    TENANT_B_CTX --> DAL
    TENANT_C_CTX --> DAL

    DAL --> FILTER
    FILTER --> DB

    DB --> DATA_A
    DB --> DATA_B
    DB --> DATA_C

    style FILTER fill:#e8f5e9

Tenant Context Resolution

sequenceDiagram
    participant Client
    participant Middleware as Auth Middleware
    participant Resolver as Tenant Resolver
    participant DB as Database
    participant Context as Request Context

    Client->>Middleware: Request with token
    Middleware->>Middleware: Validate token
    Middleware->>Resolver: Resolve tenant

    Resolver->>Resolver: Check request headers
    Note over Resolver: X-Tenant-ID header

    alt Header present
        Resolver->>DB: Verify user in tenant
        DB-->>Resolver: Membership confirmed
    else No header
        Resolver->>DB: Get user's default tenant
        DB-->>Resolver: Default tenant
    end

    Resolver->>Resolver: Check site context
    Note over Resolver: Host header or X-Site-ID

    Resolver->>DB: Load tenant settings
    DB-->>Resolver: Settings

    Resolver->>Context: Build context
    Context->>Context: Store tenantId, siteId

    Context-->>Client: Request proceeds

Data Isolation Patterns

flowchart TD
    subgraph "Row-Level Isolation"
        QUERY[Database Query]
        WHERE[WHERE clause]
        TENANT_FILTER[tenantId = ctx.tenantId]
    end

    QUERY --> WHERE
    WHERE --> TENANT_FILTER

    subgraph "Query Examples"
        Q1["SELECT * FROM products WHERE tenantId = ?"]
        Q2["UPDATE orders SET status = ? WHERE tenantId = ? AND id = ?"]
        Q3["DELETE FROM items WHERE tenantId = ? AND id = ?"]
    end

    subgraph "Protection Layers"
        L1[Middleware Filter]
        L2[Service Layer Check]
        L3[Database Constraint]
    end

    L1 --> L2 --> L3

    style TENANT_FILTER fill:#e8f5e9

Context Injection Flow

flowchart TD
    subgraph "Entry Points"
        TRPC[TRPC Procedure]
        API[REST API]
        JOB[Background Job]
        WEBHOOK[Webhook Handler]
    end

    subgraph "Context Sources"
        AUTH_CTX[Auth Context]
        HEADER_CTX[Header Context]
        JOB_CTX[Job Payload]
        HOOK_CTX[Webhook Context]
    end

    subgraph "Context Builder"
        BUILD[Build Context]
        VALIDATE[Validate Access]
        INJECT[Inject into Services]
    end

    TRPC --> AUTH_CTX
    API --> HEADER_CTX
    JOB --> JOB_CTX
    WEBHOOK --> HOOK_CTX

    AUTH_CTX --> BUILD
    HEADER_CTX --> BUILD
    JOB_CTX --> BUILD
    HOOK_CTX --> BUILD

    BUILD --> VALIDATE
    VALIDATE --> INJECT

    subgraph "Context Object"
        CTX_OBJ[Context]
        TENANT[tenantId: string]
        SITE[siteId: string]
        USER[userId: string]
        PERMS[permissions: string[]]
    end

    INJECT --> CTX_OBJ
    CTX_OBJ --> TENANT
    CTX_OBJ --> SITE
    CTX_OBJ --> USER
    CTX_OBJ --> PERMS

    style CTX_OBJ fill:#e8f5e9

Cross-Tenant Access Prevention

flowchart TD
    REQUEST[Data Request] --> EXTRACT[Extract Record ID]

    EXTRACT --> LOOKUP[Lookup Record]
    LOOKUP --> EXISTS{Record Exists?}

    EXISTS -->|No| NOT_FOUND[404 Not Found]
    EXISTS -->|Yes| CHECK_TENANT[Check Tenant Match]

    CHECK_TENANT --> MATCH{Same Tenant?}
    MATCH -->|No| FORBIDDEN[403 Forbidden]
    MATCH -->|Yes| CHECK_SITE[Check Site Match]

    CHECK_SITE --> SITE_MATCH{Site Relevant?}
    SITE_MATCH -->|Yes, Different| FORBIDDEN
    SITE_MATCH -->|Yes, Same| ALLOWED[Access Allowed]
    SITE_MATCH -->|No, N/A| ALLOWED

    ALLOWED --> RETURN[Return Data]

    style ALLOWED fill:#e8f5e9
    style FORBIDDEN fill:#ffebee
    style NOT_FOUND fill:#fff3e0

Service Layer Isolation

sequenceDiagram
    participant Router as TRPC Router
    participant Registry as Service Registry
    participant Service as Domain Service
    participant DB as Database

    Router->>Registry: getService("product", ctx)
    Registry->>Registry: Build cache key with tenantId
    Note over Registry: Key: "product:tenant-uuid"

    alt Service cached
        Registry-->>Router: Cached service instance
    else Not cached
        Registry->>Service: Create new instance
        Service->>Service: Store tenant context
        Registry->>Registry: Cache instance
        Registry-->>Router: New service instance
    end

    Router->>Service: list(filters)
    Service->>Service: Inject tenantId into query
    Service->>DB: Query with tenant filter
    DB-->>Service: Filtered results
    Service-->>Router: Tenant-scoped data

Tenant Switching

flowchart TD
    USER[User with Multi-Tenant Access] --> SELECT[Select Tenant]

    SELECT --> VERIFY[Verify Membership]
    VERIFY --> MEMBER{Is Member?}

    MEMBER -->|No| DENY[Access Denied]
    MEMBER -->|Yes| LOAD_ROLE[Load Role in Tenant]

    LOAD_ROLE --> SWITCH[Switch Active Tenant]
    SWITCH --> UPDATE_SESSION[Update Session]

    UPDATE_SESSION --> NEW_CTX[New Tenant Context]
    NEW_CTX --> RELOAD_UI[Reload UI State]

    subgraph "Session Update"
        OLD_TENANT[Old: Tenant A]
        NEW_TENANT[New: Tenant B]
        CLEAR_CACHE[Clear Service Cache]
    end

    UPDATE_SESSION --> OLD_TENANT
    OLD_TENANT --> NEW_TENANT
    NEW_TENANT --> CLEAR_CACHE

    style NEW_CTX fill:#e8f5e9

Site-Level Isolation

flowchart TD
    TENANT[Tenant Context] --> SITE_REQ[Site Request]

    SITE_REQ --> RESOLVE{Resolve Site}
    RESOLVE --> HOST[From Host Header]
    RESOLVE --> HEADER[From X-Site-ID]
    RESOLVE --> DEFAULT[Default Site]

    HOST --> LOOKUP[Lookup Site]
    HEADER --> LOOKUP
    DEFAULT --> LOOKUP

    LOOKUP --> BELONGS{Site in Tenant?}
    BELONGS -->|No| ERROR[Invalid Site]
    BELONGS -->|Yes| LOAD_CFG[Load Site Config]

    LOAD_CFG --> SITE_CTX[Site Context]

    subgraph "Site-Scoped Data"
        PRODUCTS[Site Products]
        CONTENT[Site Content]
        SETTINGS[Site Settings]
        USERS[Site Users]
    end

    SITE_CTX --> PRODUCTS
    SITE_CTX --> CONTENT
    SITE_CTX --> SETTINGS
    SITE_CTX --> USERS

    style SITE_CTX fill:#e8f5e9

Database Schema Pattern

erDiagram
    TENANT ||--o{ PRODUCT : owns
    TENANT ||--o{ ORDER : owns
    TENANT ||--o{ CONTACT : owns

    PRODUCT {
        string id PK
        string tenantId FK "REQUIRED"
        string siteId FK "OPTIONAL"
        string name
        decimal price
    }

    ORDER {
        string id PK
        string tenantId FK "REQUIRED"
        string siteId FK "REQUIRED"
        string customerId FK
        decimal total
    }

    CONTACT {
        string id PK
        string tenantId FK "REQUIRED"
        string email
        string name
    }

Audit Trail with Tenant Context

flowchart TD
    ACTION[User Action] --> CAPTURE[Capture Context]

    CAPTURE --> AUDIT_RECORD[Create Audit Record]

    AUDIT_RECORD --> WHO[Who: userId]
    AUDIT_RECORD --> WHAT[What: action]
    AUDIT_RECORD --> WHERE[Where: tenantId, siteId]
    AUDIT_RECORD --> WHEN[When: timestamp]
    AUDIT_RECORD --> DATA[Data: before/after]

    WHO --> STORE[Store Audit Log]
    WHAT --> STORE
    WHERE --> STORE
    WHEN --> STORE
    DATA --> STORE

    STORE --> QUERYABLE[Queryable by Tenant]

    subgraph "Audit Query"
        QUERY_TENANT[Filter by tenantId]
        QUERY_USER[Filter by userId]
        QUERY_DATE[Filter by date]
    end

Cross-Tenant Data Sharing

flowchart TD
    subgraph "Shared Resources"
        TEMPLATES[System Templates]
        CATALOGS[Product Catalogs]
        INTEGRATIONS[Integration Configs]
    end

    subgraph "Sharing Methods"
        COPY[Copy to Tenant]
        REFERENCE[Reference Only]
        INHERIT[Inherit + Override]
    end

    TEMPLATES --> REFERENCE
    CATALOGS --> COPY
    INTEGRATIONS --> INHERIT

    subgraph "Tenant Data"
        T1_DATA[Tenant A: Own + Shared]
        T2_DATA[Tenant B: Own + Shared]
    end

    REFERENCE --> T1_DATA
    REFERENCE --> T2_DATA
    COPY --> T1_DATA
    COPY --> T2_DATA
    INHERIT --> T1_DATA
    INHERIT --> T2_DATA

    style SHARED fill:#e1f5fe

Isolation Verification

flowchart TD
    TEST[Integration Test] --> SETUP[Setup Test Tenants]

    SETUP --> T1[Create Tenant A]
    SETUP --> T2[Create Tenant B]

    T1 --> DATA_T1[Create Data in A]
    T2 --> DATA_T2[Create Data in B]

    DATA_T1 --> QUERY_T1[Query as Tenant A]
    DATA_T2 --> QUERY_T2[Query as Tenant B]

    QUERY_T1 --> ASSERT1{Sees only A data?}
    QUERY_T2 --> ASSERT2{Sees only B data?}

    ASSERT1 -->|Yes| CROSS_CHECK[Cross-tenant attempt]
    ASSERT2 -->|Yes| CROSS_CHECK

    CROSS_CHECK --> TRY_ACCESS[A tries to access B's data]
    TRY_ACCESS --> BLOCKED{Access blocked?}

    BLOCKED -->|Yes| PASS[Test Passed]
    BLOCKED -->|No| FAIL[Test Failed - Leak!]

    style PASS fill:#e8f5e9
    style FAIL fill:#ffebee