Supabase RLS Returns Empty Row — Multi-Tenant Policy Blocking Valid User Query
The Exact Error
PostgrestError: {
code: 'PGRST116',
details: 'Results contain 0 rows',
hint: null,
message: 'JSON object requested, multiple (or no) rows returned'
}
Expected: Row for user_id = 'usr_abc123' in tenant_id = 'ten_xyz'
Received: [] (empty — RLS blocked the query silently)What's Happening
Supabase returns empty results or PGRST116 errors for users who should have access. The data exists in the DB but RLS is silently blocking the read.
Root Cause
Supabase RLS policies fail silently — they don't throw permission errors, they return empty results. In multi-tenant setups, a policy that checks `auth.uid()` against a tenant membership table can fail if the join is misconfigured, the tenant_id is missing from the JWT, or the policy references a column that doesn't exist in the anon role's accessible schema.
Quick Diagnosis Checklist
Run through these steps to confirm this is your issue:
Use Supabase RLS debugger: `SET pgrst.db_anon_role = 'authenticated'` and test directly in SQL editor
Check if `auth.uid()` matches the user_id in the RLS policy — log both to compare
Verify the tenant_id is being passed in the JWT custom claims
Test with `SECURITY DEFINER` temporarily to confirm data exists
Run `EXPLAIN (ANALYZE, FORMAT JSON) SELECT ...` to see if RLS filter is applied
The Permanent Fix
Implement automated RLS boundary tests that run in staging before every deploy — testing every tenant isolation path systematically, not just the happy path.
Drop-In Package That Permanently Fixes This
Instead of building the fix from scratch, use this production-ready package.
// rls-tester.config.ts
import { RLSTester } from './rls-tester'
const tester = new RLSTester({
supabaseUrl: process.env.SUPABASE_URL!,
serviceKey: process.env.SUPABASE_SERVICE_KEY!,
tenants: ['tenant_a_id', 'tenant_b_id', 'tenant_c_id'],
tables: ['documents', 'invoices', 'user_profiles'],
attacks: ['tenant_escalation', 'null_rls', 'jwt_tampering'],
outputReport: 'compliance-audit-log.json' // hand to auditors
})
// Run in CI before every deploy
await tester.runAll() // throws if any isolation failure found