Supabase RLS: the security guide you needed before launch
October 12, 2026 · ~8 min read
Supabase exposes your Postgres directly to the browser. That's what makes it fast — and also what makes it dangerous: your Row Level Security (RLS) isn't "another layer", it is your API's authorization. If it's loose, anyone with your anon key (public by design) reads and writes your database. Below are the 7 mistakes we see most, with the wrong SQL and the right SQL.
anon key ships in your front-end bundle — anyone copies it from DevTools. With it, they can talk to Supabase's PostgREST directly, bypassing your app entirely. RLS is the only thing deciding what that person can see. This isn't theory: there have been multiple public leaks of Supabase apps with RLS turned off.
1. RLS simply disabled on the table
Mistake number one, and the worst. Creating a table in the editor and forgetting to enable RLS leaves it wide open to the anon key — read and write everything.
-- ✅ Enable RLS on EVERY table with real data alter table public.orders enable row level security; -- Find tables without RLS: select tablename from pg_tables where schemaname='public' and rowsecurity=false;
Enabling RLS with no policy denies everything by default (safe). The danger is the table that never had RLS turned on.
2. The using (true) policy
RLS on, but with a policy that allows everyone. Looks protected in the dashboard, protects nothing.
-- ❌ Everyone reads everything create policy "read" on orders for select using (true); -- ✅ Only the owner reads create policy "owner reads" on orders for select using (auth.uid() = user_id);
3. Protecting SELECT and forgetting INSERT/UPDATE/DELETE
Postgres policies are per operation. It's common to nail reads and leave writes open — then a user inserts a row as someone else, or updates what isn't theirs. Reads use using; writes need with check.
-- ✅ Insert: the new row MUST belong to the user create policy "owner inserts" on orders for insert with check (auth.uid() = user_id); -- ✅ Update: validate the old row (using) AND the new one (with check) create policy "owner updates" on orders for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
4. Trusting a user_id the client sends
If your insert policy doesn't tie the row to auth.uid(), the client picks whatever user_id it wants in the insert and writes data as if it were someone else. The with check (auth.uid() = user_id) above closes it: identity comes from the token, not the request body.
5. The service_role key in the front-end
The service_role key bypasses RLS entirely — it's the admin key. If it leaks to the browser (in a NEXT_PUBLIC_…, say), all your RLS ceases to exist. It can only live on the server (Edge Function, API route), never in code that ships to the client.
-- ❌ in the front NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=... -- ✅ in the front NEXT_PUBLIC_SUPABASE_ANON_KEY=... (anon, protected by RLS)
6. Functions and views that bypass RLS
A security definer function runs with the privileges of its creator and ignores the caller's RLS — useful, but a footgun if exposed carelessly. Views could also surface data over the top of RLS. Prefer security invoker where you can, and review what's exposed in the public schema.
create function my_fn() returns setof orders
language sql security invoker as $$ select * from orders $$;
7. Forgetting Storage (and Realtime)
Supabase Storage buckets have their own RLS policies. Locking down tables and leaving a bucket public exposes users' files to anyone with the URL. Each private bucket needs a policy tied to the owner.
-- ✅ each user only touches their own folder in Storage create policy "owner files" on storage.objects for all using ( bucket_id='uploads' and (storage.foldername(name))[1] = auth.uid()::text );
anon key in a script and try to read another user's row. If data comes back, the policy is loose — it's exactly a multi-tenant isolation case. On Firebase the logic is the same from the other side — see the 7 Firebase Security Rules mistakes.
Don't want to audit policy by policy?
SAAS SHIELD reviews your Supabase or Firebase repository and returns every access flaw with its location, evidence and fix.
Request an audit →