Skip to main content
Version: 1.0.0-beta

Data Isolation & Multi-Tenancy

PostgreSQL RLS, org-scoped queries, and complete tenant data isolation.

Isolation Layers

Row-Level Security (RLS)

How RLS Works

PostgreSQL RLS policies are database-level guards:

-- Enable RLS on a table
ALTER TABLE engagement ENABLE ROW LEVEL SECURITY;

-- Create isolation policy
CREATE POLICY org_isolation ON engagement
USING (org_id = current_setting('app.current_org_id')::int);

Protected Tables (19 policies)

TablePolicy Type
engagementDirect org_id
workpaperDirect org_id
findingDirect org_id
evidenceDirect org_id
audit_log_eventDirect org_id
pbc_requestDirect org_id
reportDirect org_id
qa_reviewDirect org_id
monitoring_testDirect org_id
kri_thresholdDirect org_id
action_planFK via finding → org_id
test_procedureFK via workpaper → org_id
phaseFK via engagement → org_id
milestoneFK via engagement → org_id
review_pointFK via engagement → org_id
signoffFK via engagement → org_id
plan_itemFK via annual_plan → org_id
questionnaireDirect org_id
questionnaire_responseFK via questionnaire → org_id

OrgScopedDB Dependency

The OrgScopedDB FastAPI dependency:

  1. Extracts org_id from the authenticated user's JWT
  2. Creates a database session
  3. Sets SET LOCAL app.current_org_id = {org_id}
  4. All subsequent queries automatically filtered by RLS
# In routers
async def list_engagements(session: AsyncSession = OrgScopedDB):
# This query is automatically filtered by org_id
result = await session.execute(select(Engagement))
return result.scalars().all()

Storage Isolation

MinIO (object storage) uses org-prefixed paths:

bucket/org-{org_id}/evidence/{file_hash}

Network Isolation (Private Tenant)

For Enterprise/Sovereign tiers:

  • Dedicated OKE namespace
  • Network policies restricting cross-namespace traffic
  • Dedicated database instance (optional)
  • Separate MinIO bucket