CRITICAL Supabase · Postgres · Firebase (same idea, different name)

Supabase Row Level Security is off, or has no policy

What you'd see: Your app works, and the Supabase dashboard shows "RLS disabled" or "No policies" next to your tables.

§ 01 — What's actually happening

Supabase gives your frontend a public anon key and talks to Postgres directly. The only thing standing between that key and your entire table is Row Level Security — per-row rules Postgres enforces on every query. With RLS off, the anon key is a read (and often write) key for everything in the table. Anyone can open DevTools, copy the key out of your JavaScript bundle, and query your database from their own terminal.

Why an AI tool writes it this way

RLS blocks queries by default, so a half-configured policy makes the app appear broken — data stops loading. The fastest way to make the error go away is to turn RLS off, and that is very often what gets suggested when you paste the error into a chat window. The app immediately works, and nothing ever tells you what you just opened up.

What it costs you

Complete disclosure of every row: customer names, emails, messages, orders, uploaded documents. This is the single most common critical finding we see in Supabase applications, and it is also the easiest one for an outsider to discover — the key is sitting in your published JavaScript.

§ 02 — Before & after

Every user can read every other user's data

Illustrative code, written for this page — never a client's project.

AI-generated

  • RLS disabled, so the anon key can read the entire table
  • Filtering happens in the frontend, where the user controls it
  • Anyone can re-run the same query without the filter
-- RLS never enabled on the table. -- Security lives entirely in the frontend query: const { data } = await supabase .from('orders') .select('*') .eq('user_id', user.id); // remove this line and you get everyone's orders

Human-reviewed

  • RLS on, with an explicit policy per operation
  • Postgres enforces ownership on every query, whatever the client sends
  • A dropped .eq() in the frontend leaks nothing
alter table orders enable row level security; create policy "read own orders" on orders for select using (auth.uid() = user_id); create policy "insert own orders" on orders for insert with check (auth.uid() = user_id); create policy "update own orders" on orders for update using (auth.uid() = user_id) with check (auth.uid() = user_id); -- The frontend query can now stay exactly as it is. -- Postgres refuses to return anyone else's rows either way.

§ 03 — Check your own

How to tell in two minutes

You don't need us to run these. If any of them come back the wrong way, you have this problem.

  1. In the Supabase dashboard open Table Editor. Any table showing "RLS disabled" is fully readable with your public key.
  2. A table can have RLS enabled and still be wide open if a policy uses using (true). Read the policy, not just the toggle.
  3. Check every operation. It is common to see a SELECT policy with no INSERT, UPDATE or DELETE policy — or the reverse.
  4. Search your codebase for service_role. That key bypasses RLS entirely and must never appear in frontend code or in any variable prefixed NEXT_PUBLIC_ or VITE_.

Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.

Get my free health check

Find out where you actually stand.

Free. No obligation. Reviewed by experienced engineers.

Get my free health check