Security · October 8, 2026 · 3 min read

Supabase row-level security: seven mistakes that leak your data

Supabase lets your frontend talk to Postgres directly. That's what makes it fast to build with, and it's why row-level security (RLS) matters so much: it's the only thing between your public anon key and every row in your database.

These are the mistakes that come up again and again in AI-generated Supabase projects, with the fix for each.

1. RLS left off

Tables created from SQL, rather than the dashboard, can be created with RLS disabled. Supabase's Security Advisor flags these. Enable it on every table in the public schema, then add policies. A table with RLS on and no policies denies everything, which is the safe default.

alter table public.notes enable row level security;

2. Policies that say true

When an app throws permission errors, the quickest prompt-driven fix is a policy with using (true). The errors stop because everything is allowed. Replace it with a real condition.

create policy "Users read own notes" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

3. Inserts that don't check the owner

An insert policy without a with check clause lets a user create rows that claim to belong to someone else. Always pin the owner column to the signed-in user.

create policy "Users add own notes" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

4. Roles stored in user metadata

User metadata (raw_user_meta_data) can be updated by the user from the client. A policy that checks a role stored there can be bypassed by anyone who sets their own role to admin. Keep roles in a table only admins can write, or in app_metadata, which users cannot edit.

5. Views that bypass RLS

By default, a Postgres view runs with its creator's permissions, which usually means it ignores RLS on the tables underneath. On Postgres 15 and later, create views with security_invoker = true so the caller's policies apply.

create view public.note_titles with (security_invoker = true) as
  select id, title from public.notes;

6. Security definer functions in the public schema

Functions marked security definer run as their owner and skip RLS. If they live in an exposed schema, any visitor can call them through the API. Move them to a private schema, or check auth.uid() inside the function.

7. The service role key in the browser

The service role key bypasses RLS completely. It belongs only on servers and in edge functions. If it has ever shipped in client code, rotate it now.

How to check your own project

  • Run the Security Advisor in the Supabase dashboard and fix every error.
  • List every policy and read the condition out loud. If you can't explain who it allows, it's probably too broad.
  • Use the anon key from your site to query each table. Anything that comes back without signing in is public.

Deeraf's Supabase security audit does all of this for every table, bucket and function, and proves each finding with a real request.

Keep reading

Want a second pair of eyes on your app?

Book a Tech Check