If your app runs on Supabase, your database is reachable straight from the browser. That’s the point: the app talks to it directly with the anon key, which is public by design and ships in your JavaScript. The only thing standing between that public key and every row in your tables is row-level security, or RLS.
What RLS actually does
RLS is a set of rules attached to each table. Every time someone reads or writes a row, Postgres checks the rules and only lets through the rows they allow. With RLS on, a logged-in customer asking for “all invoices” gets back only their own. With RLS off, they get everyone’s.
Why it gets missed
- Tables made in the Supabase Table Editor have RLS switched on by default. Tables made with SQL, in the SQL editor or a migration, do not. You have to enable it yourself.
- An AI builder or tutorial may create tables in SQL and never mention it.
- The app looks fine. Your own screens only ask for your own data, so nothing seems wrong until someone asks the database directly.
Turn it on
For every table in the public schema:
alter table invoices enable row level security;
With RLS on and no rules yet, nobody using the public key can read or write anything. That’s the safe starting point. Now add a rule for what should be allowed. For example, “signed-in users can read their own invoices”:
create policy "Owners read their invoices"
on invoices for select
to authenticated
using ( (select auth.uid()) = user_id );
Write separate rules for insert, update and delete. An update rule usually needs both using (which rows you may touch) and with check (what the row may look like afterwards), so nobody can hand their invoice to someone else.
Check it the honest way
- In the Supabase dashboard, open Advisors → Security Advisor. It flags tables in exposed schemas that have RLS disabled.
- Make two test accounts. Sign in as the first and create some data. Sign in as the second and try to see, change or delete it through every screen and every API route your app has. You should get nothing.
- Check storage buckets too. Files have their own policies, separate from tables.
The key that skips all of this
The service_role key ignores RLS completely. It belongs only on your server, in a server route or edge function. If it’s anywhere in your frontend code or a public environment variable, rotate it now. Our note on public .env files shows how to check.
public gets RLS on, then one rule per action you actually want to allow. Anything you didn’t write a rule for stays closed.