Multi-Tenancy
Every Cloud Backend deployment serves multiple tenants from a single database. Tenant isolation is enforced at the database level using Azure SQL Server’s Row-Level Security (RLS) mechanism — not application-level filtering.
How it works
Section titled “How it works”sequenceDiagram
participant Handler as Domain Handler
participant DB as sqlx / Azure SQL
participant RLS as Row-Level Security Policy
Handler->>DB: Open connection from pool
Handler->>DB: EXEC sp_set_session_context @key=tenant_id @value=uuid
Handler->>DB: SELECT * FROM incidents
DB->>RLS: Evaluate RLS predicate for each row
RLS->>DB: Filter: WHERE tenant_id = SESSION_CONTEXT(tenant_id)
DB-->>Handler: Only rows matching tenant_id
The SESSION_CONTEXT is set at the start of every request using the tenant_id extracted from the validated JWT. This means:
- No application-level
WHERE tenant_id = ?is needed in individual queries (though some queries include it explicitly for clarity) - Accidental cross-tenant data leaks are prevented at the DB layer even if a handler forgets to filter
- The RLS policy is defined in migration files (
Backend/db/migration/)
RLS policy definition
Section titled “RLS policy definition”The Row-Level Security predicates are created in a migration file (see V14__disable_tenant_rls.sql and others). Each tenant-scoped table has:
-- Predicate function (illustrative)CREATE FUNCTION dbo.fn_rls_tenant(@tenant_id UNIQUEIDENTIFIER) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT 1 AS result WHERE CAST(SESSION_CONTEXT(N'tenant_id') AS UNIQUEIDENTIFIER) = @tenant_id;
-- Applied to a tableCREATE SECURITY POLICY rls_incidents ADD FILTER PREDICATE dbo.fn_rls_tenant(tenant_id) ON dbo.incidents, ADD BLOCK PREDICATE dbo.fn_rls_tenant(tenant_id) ON dbo.incidents;- FILTER predicate — restricts
SELECT,UPDATE,DELETEto matching rows - BLOCK predicate — prevents
INSERTorUPDATEwith a mismatchedtenant_id
Refer to Backend/docs/deployment/row-level-security.md for the full schema documentation.
Platform admin bypass
Section titled “Platform admin bypass”The cross-tenant admin console (/api/v1/admin/*) uses a separate SQL login (DATABASE_URL_PLATFORM_ADMIN) that bypasses RLS. This login should have least-privilege access and its usage is always audit-logged.
DATABASE_URL_PLATFORM_ADMIN=sqlserver://... # separate login without RLSCross-tenant background jobs (e.g., platform-wide CVE feed sync) also use this connection.
Tenant lifecycle
Section titled “Tenant lifecycle”flowchart TD
A[Tenant registers - status: pending] --> B{License activated by Management?}
B -->|No| B
B -->|Yes| C[Status: active - tenant_licenses updated]
C --> D[Users can log in - modules per license]
D --> E{License expires?}
E -->|Yes| A
E -->|No| D
Until activation, POST /api/v1/auth/login returns an error indicating the tenant is pending. This is enforced in the billing domain which checks tenant_licenses before issuing JWTs.
Data model
Section titled “Data model”| Table | Tenant-scoped | Notes |
|---|---|---|
tenants | No | Root table; contains tenant metadata |
users | Yes | Scoped to tenant |
assets | Yes | CMDB assets |
incidents | Yes | ITSM incidents |
audit_logs | Yes | Immutable audit trail |
tenant_licenses | No (keyed by tenant_id) | License state, updated by Management webhook |
tenant_features | No (keyed by tenant_id) | Feature flags per tenant |
All tenant-scoped tables include a tenant_id UUID NOT NULL column and have RLS policies applied.