Jonatas Silva

Tenants & Multi-tenant Control

Workspaces for each law firm. Data is isolated at the database layer via Supabase Row Level Security.

5 tenants

Total tenants

5

Active users

94

Combined AI spend

$23,272

Avg compliance

87%

FirmPlanStateStatusAI cost / moToken usageUsersLast publishComplianceIntegrations
HA
Hamilton & Reed LLP
us-west-2
EnterpriseCaliforniaActive$8,421
41.28M
38/5019h agoLow5/5
WE
Westbridge Legal Group
us-east-1
GrowthTexasActive$3,180
17.94M
14/2022h agoMedium4/5
MO
Morgan Trial Attorneys
us-east-1
GrowthFloridaPast Due$4,291
19.65M
11/202d agoHigh4/4
SI
Silva & Partners Legal
us-east-1
EnterpriseNew YorkActive$6,740
28.41M
27/4019h agoLow6/6
NO
Northstar Compliance Counsel
us-central-1
StarterIllinoisTrial$640
2.18M
4/53d agoMedium2/3

Supabase RLS Tenant Isolation

How tenant data leakage is prevented at the database layer.

Supabase RLS Tenant Isolation

Every tenant-scoped table carries a tenant_id column. Row Level Security policies bind reads and writes to the tenant_id claim inside the session JWT — Postgres enforces isolation at the row level, not the application layer.

tenant_id claim

auth.jwt() ->> 'tenant_id'

Policy

tenant_isolation (USING + WITH CHECK)

Enforcement

FORCE ROW LEVEL SECURITY

Leak prevention

cross-tenant rows filtered to 0

rls_policies.sql
-- Enable row level security on every tenant-scoped table
ALTER TABLE legal_posts ENABLE ROW LEVEL SECURITY;

-- Read + write isolation keyed on the JWT tenant claim
CREATE POLICY "tenant_isolation"
ON legal_posts
USING (tenant_id = auth.jwt() ->> 'tenant_id')
WITH CHECK (tenant_id = auth.jwt() ->> 'tenant_id');

-- Force RLS even for table owners (defense in depth)
ALTER TABLE legal_posts FORCE ROW LEVEL SECURITY;

Allowed vs. blocked queries

Tenant reads its own legal postsAllowed
SELECT * FROM legal_posts WHERE tenant_id = 'tnt_hamilton_reed';
-- session JWT tenant_id = 'tnt_hamilton_reed'

tenant_id in row matches auth.jwt() ->> 'tenant_id'. RLS USING clause passes.

Tenant attempts to read another tenant's postsBlocked
SELECT * FROM legal_posts WHERE tenant_id = 'tnt_westbridge';
-- session JWT tenant_id = 'tnt_hamilton_reed'

Row tenant_id ≠ JWT tenant_id. Postgres returns 0 rows — silently filtered by RLS, no error leakage.

Service role bypass for billing aggregation workerAllowed
SET LOCAL role = 'service_role';
SELECT tenant_id, SUM(cost) FROM ai_traces GROUP BY tenant_id;

service_role bypasses RLS by design. Used only inside trusted BullMQ workers, never exposed to the client.

Forged JWT with mismatched tenant claimBlocked
UPDATE legal_posts SET status = 'published' WHERE id = 'post_9912';
-- attacker JWT tenant_id = 'tnt_attacker'

WITH CHECK + USING both enforce tenant_id equality. Update affects 0 rows; attempt recorded in audit log.

Cross-tenant access attempt blocked by RLS policy.

JWT tenant_id=tnt_attacker attempted to read tnt_silva_partners.legal_posts. Postgres returned 0 rows; event recorded in the audit log with risk level critical.