Multi-Tenant Data Isolation
Auth Safety · AUTH-19 · Priority: P0
Why It Matters
In any multi-tenant SaaS application — where multiple organizations or teams share the same database — every query must be scoped to the current tenant. Without tenant isolation, User A from Company X can see, modify, or delete data belonging to Company Y.
The Moltbook breach exposed 1.5 million API tokens and 30,000+ user emails because Row Level Security was never enabled on Supabase tables — meaning any authenticated user could read every row from every tenant. Since Supabase exposes the anon_key in client bundles by design, any table without proper tenant-scoped RLS is fully readable by anyone on the internet.
AI code generators build single-tenant logic by default. They create queries like SELECT * FROM projects without adding WHERE organization_id = current_org(). When the app grows to serve multiple customers, all data becomes shared.
Priority: P0 — Cross-tenant data exposure is a breach. One incident destroys customer trust permanently.
Affected Stack: Supabase, Next.js, any multi-tenant SaaS architecture
The Problem
AI tools generate database queries and RLS policies that filter by auth.uid() (the individual user) but not by organization_id or tenant_id. This works for single-user apps but breaks immediately when organizations have multiple members.
-- ❌ What AI tools typically generate — user-level only
CREATE POLICY "Users can see their projects"
ON projects FOR SELECT
USING (auth.uid() = created_by);
This policy lets User A see only their own projects — but not projects created by their teammates. So the AI "fixes" it:
-- ❌ Common AI "fix" — removes all restrictions
CREATE POLICY "Users can see all projects"
ON projects FOR SELECT
USING (true);
Now every user in every organization can see every project.
The Fix
Implement tenant-scoped RLS policies that check organization membership, not just individual user identity.
Step 1: Store organization membership
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES auth.users(id),
role TEXT DEFAULT 'member',
UNIQUE(organization_id, user_id)
);
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
Step 2: Create tenant-scoped RLS policies
-- ✅ Production-safe: users can only see projects from their organization
CREATE POLICY "Users can see org projects"
ON projects FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid()
)
);
Step 3: Scope every query
// ✅ Server-side query always includes tenant scope
const { data: projects } = await supabase
.from('projects')
.select('*')
.eq('organization_id', currentOrg.id); // Explicit tenant filter
Key rules:
- Every table with tenant data must have
organization_id(or equivalent) - RLS policies must check organization membership, not just
auth.uid() - Server-side queries should include explicit tenant filters as defense-in-depth
- Test with multiple organizations to verify isolation — not just multiple users in one org
- Consider using Supabase's
app_metadatato store the current org for RLS helper functions
References
- Supabase: Multi-Tenancy with RLS
- OWASP: Broken Access Control (A01:2021)
- CWE-639: Authorization Bypass Through User-Controlled Key
Related Checks
- Supabase RLS Safety — AUTH-02, AUTH-03, AUTH-17
- service_role Key Exposure — AUTH-01, ADM-17
- Server-Side Auth for Protected Routes — AUTH-04
Is your app safe? Run Free Scan →