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:
| Table | Description |
|---|---|
users | Organization members |
agents | Registered AI agents |
evidence_artifacts | Compliance evidence |
security_assessments | Security evaluations |
security_findings | Vulnerability findings |
finding_control_mappings | Finding-to-control links |
agent_interactions | Agent activity log |
audit_log | Immutable audit trail |
policies | Organizational policies |
risk_register | Risk entries |
integrations | External integrations |
subscriptions | Billing subscriptions |
tickets | Remediation tickets |
reports | Generated reports |
members | Team membership |
invites | Pending invitations |
mcp_sessions | MCP session state |
Global Tables (No RLS)
Some tables are shared across all organizations and do not have RLS:
| Table | Reason |
|---|---|
frameworks | Compliance frameworks are global reference data |
unified_controls | Unified control taxonomy is shared |
control_mappings | Framework-to-unified mappings are shared |
evidence_requirements | Evidence type definitions are shared |
organizations | Organization 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:
- Network -- Backend services are not publicly accessible; only the API Gateway has a public endpoint
- Authentication -- Every request must be authenticated (JWT or API key)
- Application -- Services extract
org_idfrom the authenticated context, never from user input - Database -- RLS policies filter all queries to the current tenant
- 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:
- A new
organizationsrow is created with a unique UUID - A
usersrow is created linking the user to the organization - All subsequent queries for this user are scoped to their organization via RLS
- 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