Home › Blog › Supabase RLS

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.

Understand the threat: the 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 );
How to audit your RLS today: run the query in #1 to find tables without RLS; open the Security Advisor in the Supabase dashboard (it lists exposed tables); and actually test it — use the 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 →