Skip to main content

Multi-Tenancy & Data Isolation

Claw GRC uses PostgreSQL Row-Level Security (RLS) to enforce strict data isolation between organizations. This page explains how multi-tenancy works and why it provides stronger isolation than application-level filtering.

Architecture​

Every tenant table in the database includes an org_id column that references the organizations table. PostgreSQL RLS policies ensure that queries only return rows belonging to the current organization.

How RLS Works​

Step 1: Authentication​

The API Gateway authenticates the request (via JWT or API key) and extracts the organization ID from the authenticated context.

Step 2: Set Tenant Context​

Before executing any query, the backend service sets the PostgreSQL session variable:

Go (Compliance Engine):

func WithOrgContext(ctx context.Context, db *sql.DB, orgID string) error {
_, err := db.ExecContext(ctx,
"SELECT set_config('app.current_org', $1, true)",
orgID,
)
return err
}

Python (Security Engine, Agent Protocol):

async def set_rls_context(db: AsyncSession, org_id: str) -> None:
await db.execute(
text("SELECT set_config('app.current_org', :org_id, true)"),
{"org_id": org_id},
)

Step 3: RLS Policy Enforcement​

Every tenant table has an RLS policy:

CREATE POLICY tenant_isolation ON agents
USING (org_id = current_setting('app.current_org', true)::uuid);

When a query runs, PostgreSQL automatically appends this filter to every SELECT, UPDATE, and DELETE operation. The application code does not need to include WHERE org_id = ... in its queries -- the database handles it.

Step 4: FORCE ROW LEVEL SECURITY​

Claw GRC goes a step further by enabling FORCE ROW LEVEL SECURITY on all tenant tables:

ALTER TABLE agents FORCE ROW LEVEL SECURITY;

Without FORCE, the table owner role bypasses RLS policies. With FORCE, even the table owner must set the tenant context before querying. This prevents accidental data leaks from database administrators or migration scripts.

Tenant Tables​

The following 20+ tables are RLS-protected:

TableDescription
usersOrganization members
agentsRegistered AI agents
evidence_artifactsCompliance evidence
security_assessmentsSecurity evaluations
security_findingsVulnerability findings
finding_control_mappingsFinding-to-control links
agent_interactionsAgent activity log
audit_logImmutable audit trail
policiesOrganizational policies
risk_registerRisk entries
integrationsExternal integrations
subscriptionsBilling subscriptions
ticketsRemediation tickets
reportsGenerated reports
membersTeam membership
invitesPending invitations
mcp_sessionsMCP session state

Global Tables (No RLS)​

Some tables are shared across all organizations and do not have RLS:

TableReason
frameworksCompliance frameworks are global reference data
unified_controlsUnified control taxonomy is shared
control_mappingsFramework-to-unified mappings are shared
evidence_requirementsEvidence type definitions are shared
organizationsOrganization lookup (protected by application logic)

What This Means for Security​

Bug-Proof Isolation​

Because isolation is enforced at the database level, application bugs cannot leak data across tenants. Even if a backend service has a code path that fails to check the organization, the database query will return zero rows without the tenant context set.

DBA Safety​

With FORCE ROW LEVEL SECURITY, database administrators querying tables directly must set the tenant context:

-- Without this, all tenant tables return 0 rows
SELECT set_config('app.current_org', 'YOUR_ORG_UUID', true);

-- Now queries work
SELECT * FROM agents;

Defense in Depth​

RLS is one layer in a multi-layer isolation strategy:

  1. Network -- Backend services are not publicly accessible; only the API Gateway has a public endpoint
  2. Authentication -- Every request must be authenticated (JWT or API key)
  3. Application -- Services extract org_id from the authenticated context, never from user input
  4. Database -- RLS policies filter all queries to the current tenant
  5. API Gateway -- Internal headers (X-Org-ID, X-Actor-ID) are stripped from incoming requests and set from the authenticated context

Organization Lifecycle​

When a new user signs up:

  1. A new organizations row is created with a unique UUID
  2. A users row is created linking the user to the organization
  3. All subsequent queries for this user are scoped to their organization via RLS
  4. The organization UUID is embedded in the user's Firebase custom claims

Compliance Implications​

Multi-tenancy with RLS satisfies requirements across multiple frameworks:

  • SOC 2 CC6.1 -- Logical access controls enforce data isolation
  • ISO 27001 A.9.1.1 -- Access control policy implemented at the database level
  • HIPAA 164.312(a)(1) -- Technical safeguards for access control
  • GDPR Article 32 -- Technical measures ensuring ongoing confidentiality