Skip to content

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.

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/)

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 table
CREATE 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, DELETE to matching rows
  • BLOCK predicate — prevents INSERT or UPDATE with a mismatched tenant_id

Refer to Backend/docs/deployment/row-level-security.md for the full schema documentation.

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 RLS

Cross-tenant background jobs (e.g., platform-wide CVE feed sync) also use this connection.

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.

TableTenant-scopedNotes
tenantsNoRoot table; contains tenant metadata
usersYesScoped to tenant
assetsYesCMDB assets
incidentsYesITSM incidents
audit_logsYesImmutable audit trail
tenant_licensesNo (keyed by tenant_id)License state, updated by Management webhook
tenant_featuresNo (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.