Multi-tenancy
Organization → tenant → user. A single Postgres cluster holds every tenant; row-level security enforces isolation at the database layer.
The hierarchy
Draxis has three levels:
- Platform — run by Draxis.ai. Hosts everything. The only
superadmins live here. - Organization — a company or an MSP / vCISO partner. Holds one or more tenants. Has
master_admins that can manage tenants inside the org. - Tenant — one customer’s risk environment. Has
tenant_admins andtenant_users scoped to that tenant only.
Roles & permissions
| Role | Scope | Can do |
|---|---|---|
superadmin | Platform | Create orgs, impersonate any tenant, see platform audit log. |
master_admin | Organization | Create/delete tenants in the org, manage users, impersonate tenants in the org. |
tenant_admin | Tenant | Manage tenant users, configure integrations, edit the risk model. |
tenant_user | Tenant | Read the dashboard; permissions on individual writes are gated by requirePermission(...). |
carrier_admin / carrier_user | Carrier | Access /api/carrier/* routes only. See Expert panel for how carrier context is scoped. |
Data partitioning
All tenant data lives in a single Cloud SQL for PostgreSQL cluster. Every tenant-scoped table carries a tenant_id column (UUID), and every tenant-scoped table has a row-level security (RLS) policy that admits a row only when it matches the session’s tenant.
Logical layout:
- Platform schema —
organizations,tenants,users,sessions,platform_audit_logs,mfa_policy. RLS here is stricter: tenant sessions see only their own tenant / org; superadmin sessions can read across. Writes are gated by role claims on the JWT. - Tenant schema —
kri,kri_source,kri_observation,risk,control,outcome,vendor,assessment,workflow,panel_session,institutional_knowledge, and so on. Every row carriestenant_id; every table hasENABLE ROW LEVEL SECURITYplus a policy matching on it. - Carrier schema — a narrower surface scoped to the insurance-carrier portal, with its own RLS policies keyed to
carrier_id.
How the session is scoped
Every authenticated request begins with the application issuing:
SET LOCAL draxis.tenant_id = '<uuid from JWT claim>';
RLS policies reference that setting:
CREATE POLICY tenant_isolation ON kri
USING (tenant_id = current_setting('draxis.tenant_id')::uuid);
The guarantee — “cross-tenant reads are impossible” — is enforced by Postgres itself, not by application logic. Even an outright bug in an app query cannot return rows for the wrong tenant; the database refuses them.
Cross-tenant rollups
Org admins and platform admins need to read across tenants — e.g. /api/org/tenants/trends. This runs under a session that:
- Requires a
superadminormaster_adminJWT claim. - Sets an
organization_idsession variable instead oftenant_id. - Uses RLS policies on
organizations-scoped views that admit any tenant belonging to the caller’s org. - Writes a row to
platform_audit_logsrecording the cross-tenant read and the specific query.
No superuser session and no SET row_security = off is ever issued from application code. The privilege escalation is to a wider RLS scope, not to escape RLS.
Impersonation
Superadmins can impersonate any tenant via POST /api/platform/organizations/<slug>/impersonate. Master-admins can impersonate tenants inside their own org.
Implementation-wise, impersonation issues a short-lived JWT with an impersonated_tenant_id claim and an impersonator_user_id claim. The RLS session variable is set from the impersonated tenant; the audit log row carries both identities. The UI renders a persistent banner indicating impersonation mode so the operator cannot mistake it for a normal session.
MFA policy
MFA is enforced at four policy scopes, applied in order:
- Platform-wide default.
- Organization override (
PATCH /api/org/settings/mfa-policy). - Tenant override (
PATCH /api/tenants/<slug>/mfa-policy). - Per-user override (
PATCH /api/users/<id>/mfa-override, master-admin+ only).
The strictest applicable policy wins. Users without MFA enrolled are forced through enrollment on next login when policy requires it.
Adding / removing tenants
- Create — one
INSERTintotenants. No filesystem operation, no new database to provision, no migration to run on N databases. - Soft-delete — set
deleted_at. RLS policies exclude soft-deleted tenants from ordinary sessions. Data remains for the tenant’s contractual retention window. - Hard-delete — after retention expiry, a Cloud Run job deletes every row with that
tenant_idin a single transaction across all tenant tables, then vacuums. Audit-logged; reversible only from backups. - Export — a
pg_dump-based export filtered bytenant_idproduces a complete snapshot of one tenant’s data for portability or legal-hold requests.
What this replaces
Earlier Draxis deployments used one SQLite database file per tenant for strict isolation. That model was simple and explicit, but it forced a single-writer compute instance, made cross-tenant rollups expensive (opening N files), and required filesystem plumbing to add or remove tenants. The Cloud SQL + RLS model keeps the same isolation guarantee — enforced by Postgres rather than by process-local routing — while enabling horizontal scaling, native cross-tenant queries, and zero-downtime rolling deploys.