What is RLS bypass?

Updated

Reading or writing a row that should be filtered by Row-Level-Security policy. Caused by missing RLS, missing policy, broken policy logic (USING without WITH CHECK on INSERT), service-role key in client, or anon-role grants too broad.

Full explanation

RLS bypass is the canonical Supabase failure mode. Five common shapes: (1) RLS not enabled on the table — every row is readable; (2) RLS enabled but no policies — fail-closed semantics depend on Postgres version and statement type; (3) USING clause without WITH CHECK on INSERT-permitting policies — users insert rows claiming someone else's auth.uid(); (4) service-role key shipped in client bundle — bypasses every policy; (5) overly-broad GRANT to anon role — anon reads tables that should require authentication. AI-generated code ships at least one of these in a meaningful percentage of vibe-coded Supabase apps.

Example

A multi-tenant SaaS with `using (auth.uid() = user_id)` on the orders table. The policy filters by user, not by tenant. If two tenants share a user_id (common with auth-provider SSO), tenant A's user can read tenant B's user's orders. The fix: scope by both user AND tenant: `using (auth.uid() = user_id AND tenant_id = (auth.jwt() ->> 'tenant')::uuid)`.

Related

FAQ

Why doesn't Supabase warn me when RLS is missing?

Supabase shows a UI badge in the Authentication > Policies dashboard for tables without RLS. The dashboard is easy to miss when you have many tables, and migrations + AI-generated code often skip the `enable row level security` statement. Automated review catches what the dashboard does not.

Is RLS enough for multi-tenant security?

RLS is necessary but not sufficient. Defense in depth: RLS at the database, app-layer auth check at the route, WITH CHECK clauses on writes, AND tenant-scoped JWT claims. Any one layer breaking is a bug; all four together is correct.