ASAASA Standard
Not in Phase 1Production Foundation

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_metadata to store the current org for RLS helper functions

References


Related Checks


Is your app safe? Run Free Scan →