Executive Summary

AIUsagePlatform implements shared-database multi-tenancy with row-level security. Each tenant's data is isolated via OrganizationId foreign keys across all tenant-scoped tables. The MultiTenantIsolationMiddleware enforces this isolation at the request level.

βœ… Row-Level Security
OrganizationId FK on all tenant tables
βœ… Middleware Enforcement
Automatic tenant context extraction
βœ… SuperAdmin Bypass
Controlled cross-tenant access for admins
⚠️ No DB Row Security
Relies on app layer, not DB RLS

πŸ—οΈ Multi-Tenant Architecture

Shared Database Multi-Tenancy Model ═══════════════════════════════════════════════════════════════════════════════ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SINGLE DATABASE - MULTIPLE TENANTS β”‚ β”‚ (Shared Schema Approach) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Organizations Table β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Id β”‚ Name β”‚ Plan β”‚ Status β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”‚ β”‚ org-acme β”‚ Acme Corp β”‚ Pro β”‚ Active β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ org-tech β”‚ TechStart Inc β”‚ Basic β”‚ Active β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ org-global β”‚ GlobalCo β”‚Enteprise β”‚Suspended β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Users Table (Tenant-Scoped) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Id β”‚ Email β”‚ OrganizationIdβ”‚ Role β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”‚ β”‚ usr-john β”‚john@acme.com β”‚ org-acme β”‚ Admin β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ usr-jane β”‚jane@acme.com β”‚ org-acme β”‚ User β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ usr-bob β”‚ bob@tech.com β”‚ org-tech β”‚ Admin β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ ⚠ CRITICAL: Every tenant-scoped table has OrganizationId FK β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Sessions Table (Tenant-Scoped) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Id β”‚ UserId β”‚ OrganizationIdβ”‚ Start β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”‚ β”‚ ses-001 β”‚ usr-john β”‚ org-acme β”‚ 2026-01-15 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ ses-002 β”‚ usr-jane β”‚ org-acme β”‚ 2026-01-15 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ ses-003 β”‚ usr-bob β”‚ org-tech β”‚ 2026-01-15 β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Query Pattern: β”‚ β”‚ β”‚ β”‚ SELECT * FROM Sessions WHERE OrganizationId = @orgId β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Tenant Isolation Benefits: β”‚ β”‚ β€’ βœ… Single codebase for all tenants β”‚ β”‚ β€’ βœ… Simplified backups and migrations β”‚ β”‚ β€’ βœ… Cross-tenant analytics possible (SuperAdmin) β”‚ β”‚ β€’ ⚠️ Risk of data leakage if queries miss WHERE OrganizationId clause β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”Œ MultiTenantIsolationMiddleware

Middleware Execution Flow ═══════════════════════════════════════════════════════════════════════════════ Incoming Request β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MultiTenantIsolationMiddleware (Custom) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Step 1: Bypass Check β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ IF path is in bypass list (auth, swagger, health, agent-onboarding) β”‚ β”‚ β†’ Skip tenant validation, continue to next middleware β”‚ β”‚ β”‚ β”‚ Step 2: Extract Tenant Context β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ Primary: Extract OrganizationId from JWT claims (sub claim) β”‚ β”‚ Fallback: Check X-Organization-Key header (for service accounts) β”‚ β”‚ β”‚ β”‚ IF no OrganizationId found β”‚ β”‚ β†’ Return 401 Unauthorized β”‚ β”‚ β”‚ β”‚ Step 3: SuperAdmin Check β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ IF user has SuperAdmin role β”‚ β”‚ β†’ BYPASS all tenant checks (explicit permission) β”‚ β”‚ β†’ Set IsSuperAdmin = true in TenantContext β”‚ β”‚ β†’ Continue (SuperAdmin can access all tenants) β”‚ β”‚ β”‚ β”‚ Step 4: Validate Organization β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ Query Organization table: β”‚ β”‚ SELECT * FROM Organizations WHERE Id = @orgId β”‚ β”‚ β”‚ β”‚ IF organization not found β”‚ β”‚ β†’ Return 403 Forbidden "Invalid organization" β”‚ β”‚ β”‚ β”‚ IF organization.Status != Active β”‚ β”‚ β†’ Return 403 Forbidden "Organization suspended/expired" β”‚ β”‚ β”‚ β”‚ Step 5: Subscription Validation β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ Check Subscription: β”‚ β”‚ SELECT * FROM Subscriptions β”‚ β”‚ WHERE OrganizationId = @orgId AND Status = 'Active' β”‚ β”‚ AND EndDate > GETUTCDATE() β”‚ β”‚ β”‚ β”‚ IF no active subscription β”‚ β”‚ β†’ Return 402 Payment Required "Subscription expired" β”‚ β”‚ β”‚ β”‚ Step 6: Feature Gate Check (Optional) β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ IF endpoint has [RequiresPlanFeature] attribute β”‚ β”‚ β†’ Check if feature is enabled in organization's plan β”‚ β”‚ β†’ Return 403 if feature not available β”‚ β”‚ β”‚ β”‚ Step 7: Rate Limiting Check β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ Check Redis for rate limit counters per OrganizationId β”‚ β”‚ β†’ Return 429 Too Many Requests if exceeded β”‚ β”‚ β”‚ β”‚ Step 8: Attach Tenant Context β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ Store in HttpContext.Items: β”‚ β”‚ β€’ "OrganizationId" β†’ extracted org ID β”‚ β”‚ β€’ "IsSuperAdmin" β†’ true/false β”‚ β”‚ β”‚ β”‚ Attach Response Headers: β”‚ β”‚ β€’ X-Tenant-Id: org_xxx β”‚ β”‚ β€’ X-Tenant-Status: Active β”‚ β”‚ β”‚ β”‚ Step 9: Continue Pipeline β”‚ β”‚ ───────────────────────────────────────────────────────────────────────── β”‚ β”‚ β†’ Call next middleware/controller β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ›‘οΈ Isolation Enforcement Points

LayerEnforcement MechanismResponsibility
Middleware MultiTenantIsolationMiddleware Extract tenant, validate status, attach context
Controller BaseApiController.GetCurrentOrganizationId() Access tenant context from claims
Service OrganizationId parameter on all methods Pass tenant context to repositories
Repository WHERE OrganizationId = @orgId on all queries Enforce row-level isolation
Database Foreign key constraints Referential integrity, cascade delete

πŸ“Š Tenant Data Access Patterns

Standard User Flow
User Login ↓ JWT Created: { sub: usr-123, org: org-456 } ↓ Request /api/sessions ↓ Middleware: Extract org-456 from JWT ↓ Validate org-456 exists & Active ↓ Repository Query: SELECT * FROM Sessions WHERE OrganizationId = 'org-456' AND UserId = 'usr-123' ↓ Return: Only user's org sessions
SuperAdmin Flow
SuperAdmin Login ↓ JWT Created: { sub: usr-admin, role: SuperAdmin } ↓ Request /api/adminanalytics ↓ Middleware: Detect SuperAdmin role ↓ BYPASS tenant validation ↓ Set IsSuperAdmin = true ↓ Service Query: SELECT * FROM Sessions (no WHERE OrganizationId!) ↓ Return: All tenants' data

⚠️ Security Risks & Mitigations

⚠
Missing WHERE Clause
Developers may forget OrganizationId filter. Mitigation: Code review, integration tests, repository base class enforcement.
⚠
SuperAdmin Privilege Escalation
Compromised SuperAdmin account = full platform access. Mitigation: IP whitelisting, MFA, audit logging.
βœ“
Defense in Depth
Multiple enforcement layers (middleware, service, repository) reduce single-point-of-failure risk.
βœ“
Status Validation
Suspended/expired organizations automatically blocked at middleware layer before reaching business logic.

βœ… Recommended Enhancements

Repository Base Class

Create TenantRepositoryBase that automatically applies OrganizationId filter to all queries.

Recommended

Integration Tests

Automated tests that verify Tenant A cannot access Tenant B's data for every endpoint.

Recommended

SQL Server RLS

Enable Row-Level Security policies in SQL Server as additional defense layer.

Recommended