Supabase RLS Was Off and Anyone Could Read Everything
· Fortus Team · 5 min read
Picture this: you launch on Supabase, auth works, your dashboard shows your own data, and you ship. Then imagine a curious user opens DevTools, queries the same table directly with their own authenticated session, and reads everyone else's rows. Here's a scenario I keep seeing: Row Level Security was never enabled, so authentication was the only gate — and authentication only proves who you are, not which rows you may see. This is the most common Supabase footgun for vibe-coded apps, and it survives because the happy path hides it. With one user — you — everything looks correct. Your queries return your rows. Nothing errors. The missing policies only matter when a second user exists, and by then the app is already live. Tutorials that disable RLS to get started make it worse, because the disable step looks like normal setup rather than a security hole left open.
What RLS is and why auth alone is not enough
Supabase sits on Postgres, and Row Level Security is the Postgres mechanism that filters rows per requester. When enabled with owner-scoped policies, a query for "all orders" silently becomes "all orders belonging to the authenticated user." The database enforces this regardless of what the client asked for. Without it, any authenticated session using the standard anon key can read, and often write, any row the API exposes. Authentication gets the user through the door. Policies decide which rooms they may enter. Many builders assume the login step covers both, because no error ever suggests otherwise. Two adjacent traps compound the damage. Permissive policies like "allow all for authenticated users" look like policies while granting everything — they satisfy a checklist without enforcing ownership. And the service-role key bypasses all policies by design. If that key ever reaches frontend code or an agent-readable config, every policy you wrote stops mattering for whoever holds it.
Where RLS lives in your repo
RLS is decided in migrations and enforced at query time, so the repo tells the full story if you know where to look. Start with the migration files: table definitions, enablement statements, and policy statements. Every table holding user data should have Row Level Security enabled plus owner-scoped policies for each operation — select, insert, update, and delete — typically comparing the row's owner column against the authenticated user id. A table with no enablement, no policies, or a blanket allow-all policy is the finding. Then check the app side. Which key does client code use. Frontend and client-side queries must use the anon key so policies apply. Any service-role key reference outside tightly held server code is a separate, urgent problem. Trace one query path end to end: the client call, the table it touches, and the policy covering that table and operation. If any link is missing, the chain is open. One caveat stays honest: policies applied by hand in the dashboard without being captured in migrations are invisible to file review. If your migrations show no RLS, the fix is not just to click something in the console — it is to codify the policies in migrations so the next review sees them.
A manual self-test with two accounts
Here is a concrete self-test you can run manually on your own app. You need two test accounts and nothing else. Create user A and user B with clearly labeled rows — a note, an order, a profile, whatever your app stores per user. Log in as A in one browser session and as B in another. As A, write down the ids of A's rows. Then, as B, attempt to read A's rows directly: through the app's own screens where possible, and through direct client queries using B's session otherwise. Repeat for writes and deletes. If B can read, modify, or remove A's data anywhere, RLS is missing or permissive on that table. Then confirm in the repo. Open the migrations for each exposed table and check for the enablement statement and the per-operation owner policies. Check that frontend code uses the anon key and that the service-role key appears nowhere client-reachable. Fix by enabling RLS on every user-data table, adding owner-scoped policies for all four operations, codifying everything in migrations, and re-running the two-account test until user A cannot touch user B's rows in any direction.
Where a file scanner fits
A source-file scanner handles the repo half of this well. It can read migrations for tables without enablement or policies, flag blanket allow-all policies, trace which key client code uses, and pair each table with the query paths that reach it — each finding carrying a file path and excerpt. The dashboard half stays manual. A scanner cannot see hand-applied console policies, so when migrations show gaps but console configuration might exist, the honest result is a review prompt to codify those policies into migrations. Treat the scanner as the inventory that never tires: every table, every operation, every key reference, checked on every commit. Your two-account test remains the ground truth that proves the policies hold.
Verify Row Level Security across all tables
Fortus inspects your Supabase schema and migrations to detect unprotected tables and permissive policies before your app ships.
or learn more about Fortus →