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:#e8f5e9Tenant 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 proceedsData 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:#e8f5e9Context 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:#e8f5e9Cross-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:#fff3e0Service 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 dataTenant 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:#e8f5e9Site-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:#e8f5e9Database 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]
endCross-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:#e1f5feIsolation 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