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 and tenant_users scoped to that tenant only.

Roles & permissions

RoleScopeCan do
superadminPlatformCreate orgs, impersonate any tenant, see platform audit log.
master_adminOrganizationCreate/delete tenants in the org, manage users, impersonate tenants in the org.
tenant_adminTenantManage tenant users, configure integrations, edit the risk model.
tenant_userTenantRead the dashboard; permissions on individual writes are gated by requirePermission(...).
carrier_admin / carrier_userCarrierAccess /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 schemaorganizations, 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 schemakri, kri_source, kri_observation, risk, control, outcome, vendor, assessment, workflow, panel_session, institutional_knowledge, and so on. Every row carries tenant_id; every table has ENABLE ROW LEVEL SECURITY plus 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:

  1. Requires a superadmin or master_admin JWT claim.
  2. Sets an organization_id session variable instead of tenant_id.
  3. Uses RLS policies on organizations-scoped views that admit any tenant belonging to the caller’s org.
  4. Writes a row to platform_audit_logs recording 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:

  1. Platform-wide default.
  2. Organization override (PATCH /api/org/settings/mfa-policy).
  3. Tenant override (PATCH /api/tenants/<slug>/mfa-policy).
  4. 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 INSERT into tenants. 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_id in a single transaction across all tenant tables, then vacuums. Audit-logged; reversible only from backups.
  • Export — a pg_dump-based export filtered by tenant_id produces 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.