What disabling RLS actually exposes
When you disable Row Level Security on a Supabase table, the practical result is concrete and immediate: every row in that table becomes readable through the auto-generated REST endpoint by anyone holding the anon key. That is the whole table, including rows that belong to other users. This is the supabase disable RLS production risk, and it lands the moment the public URL exists, not at some later attack stage.
The reason it is immediate is that the anon key is not a secret. Supabase ships it in the client bundle so the browser can talk to the database, which means any visitor who opens DevTools, plus any crawler or script that loads the page, has it. With RLS off, that key is enough to read the table end to end.
It helps to separate what is observable from what is only possible. Observable: with RLS off, a GET to /rest/v1/your_table with the anon key returns rows. That is reproducible on your own project. Only possible, depending on your schema: those rows contain email addresses, message bodies, or order records that belong to other accounts. The first is a fact about how PostgREST works; the second depends on what you stored.
- RLS disabled on a table: the REST endpoint serves rows to the anonymous role, no login required.
- The exposure is per table. One closed table tells you nothing about the next one.
- This is an authorization gap, who may read which rows, not a broken login. The anon key authenticates the request as anonymous exactly as designed; the missing layer is the rule that filters which rows it gets back.
Why AI-built apps disable RLS by accident
RLS is opt-in per table on Supabase. A table created without an explicit policy has no row-level rules, and the generated REST API will serve it to the anon role. Sometimes RLS is left off because it was never turned on; sometimes a builder disables it on purpose to get past a query that returned an empty array during development, then forgets to turn it back on.
That second path is the trap. An AI-generated build wires the client SDK to read and write with the anon key, so the app works on the first try. When a read comes back empty because RLS is on with no matching policy, the fastest-looking fix is to disable RLS and watch the data appear. The app now works, so the change looks correct from the inside. supabase rls not enabled is invisible when you are testing as the developer who can see everything anyway.
- The table works in the app, so the missing protection produces no error to notice.
- Disabling RLS to unblock a query is a one-line change that fixes the symptom and removes the boundary at the same time.
- Fast deploy means the public URL is live before anyone audits which tables are reachable. A table with no row level security looks identical to a working one until someone queries it from outside.
Check each table from outside before launch
You can confirm the state of your own tables without attacking anything. Run these against your own project only.
From the Supabase dashboard, open the Advisors section and the database linter, sometimes called the Supabase security advisor. It flags tables that are exposed through PostgREST with RLS disabled. The Authentication and Database views also show, per table, whether RLS is enabled and which policies exist. A table listed as RLS-disabled is the suspicious result and the one to fix first.
From the public surface, find the project URL and anon key, which are in the client bundle, by opening DevTools, the Network tab, and watching requests to your-project.supabase.co/rest/v1/. Then query a table directly as the anonymous caller, for example a GET to /rest/v1/your_table?select=* with the anon key in the apikey header.
- Suspicious result: rows come back from a table that should require a logged-in user. Anon can read it.
- Safer result: an empty array or a permission error for tables that should be private. An empty array with RLS on and no matching policy is the expected deny, not a bug.
- Check writes too. A POST or PATCH that succeeds with the anon key means the table is writable by anyone, not only readable.
- Repeat per table. RLS is per-table, so one protected table tells you nothing about the rest.
How to fix it, and what the fix does not cover
The fix has two stages, and the first one is the safe place to land. Enable RLS on every table that holds user, account, payment, or admin data. Enabling RLS with no policy denies all access. That can look like breakage, but it is the correct starting point: the table is closed, and you then open only the paths you intend. Starting from deny-all and adding policies is far safer than leaving a table open while you decide.
Then write explicit policies per table for the operations you actually want. A policy that scopes reads to the row owner, for example matching the authenticated user id against an owner column, lets a logged-in user see their own rows and no one else's. Write one policy per intended operation rather than a single broad rule, and avoid a permissive USING (true) policy, which technically enables RLS while letting every row through, so the effect is the same as having it off. Keep privileged work off the anon role entirely; it belongs behind the service_role key, which bypasses RLS and must never ship to the client bundle.
What the fix does not cover: RLS controls which rows a caller can read or change. It does not validate the contents of a write. A user allowed to insert their own row can still submit malformed or abusive values, so input validation and constraints are a separate job. RLS also does not protect data your application code exposes through a server endpoint that uses the service_role key, because that path skips RLS by design.
Enabling RLS closes the row-level boundary, not every boundary. It is an authorization control, not authentication, not input validation, and not a substitute for reviewing the server-side code paths that run with elevated keys. Get RLS on first, then keep checking the other surfaces.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check queries your Supabase REST endpoint with the anon key from the public surface and reports tables that return rows without authentication. If a table answers the anonymous caller with data, that is a direct signal of disabled or missing RLS, and the scan flags it as a launch blocker with severity and fix direction. It checks the exact thing an outside visitor can reach, before the URL spreads.
What it does not do is write your policies or read your policy logic. Deciding which rows each role should see, and authoring correct per-table RLS, is work you do inside Supabase, and it should pair with manual review of your auth model and your service_role code paths. A clean Launch Check on the public surface means no table answered the anon caller with rows during the scan; it does not prove every policy matches your intent.
A clean scan is one signal, not a guarantee. It confirms what is reachable from outside with the anon key, not whether your policies are correct or whether an elevated server path leaks the same data another way. Treat a flagged table as a launch blocker, treat a clean result as a single green light, and list your tables to confirm RLS is enabled on each one holding private data. Run a focused Launch Check against the public URL before sharing it.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.