Supabase Row Level Security (RLS): lock down your tables
Supabase exposes your Postgres tables directly to the browser through its Data API, authenticated with the public anon (publishable) key. Row Level Security (RLS) policies are what decide which rows each user may read or change. A table in the public schema without RLS is readable — and often writable — by anyone who opens your site.
What it is
Every Supabase project ships an anon (or sb_publishable_) key that is meant to live in your frontend. It identifies your project, not your user — anyone can copy it from your JavaScript bundle and send requests to https://<project>.supabase.co/rest/v1/ themselves, bypassing your UI completely.
Row Level Security is a Postgres feature that attaches policies to a table: SQL conditions that are checked for every row on every SELECT, INSERT, UPDATE and DELETE. With RLS enabled and no policies, nothing is accessible through the API. Each policy you add opens exactly one door — for example "signed-in users may read rows where user_id matches their own id".
Why it matters
A missing or overly broad policy is a full data breach, not a minor bug: user emails, profiles, orders, private messages — whatever is in the table — can be dumped with a single curl command. AI coding tools frequently create tables through SQL migrations, where RLS is off by default, and generate permissive placeholder policies such as using (true) just to make the app work.
The service_role (sb_secret_) key bypasses RLS entirely. If it ends up in client-side code, every policy you wrote is irrelevant — anyone holding it has full read/write access to your database.
How to fix it
Run this in the Supabase SQL editor to list every table in the exposed public schema that still has RLS switched off. The Security Advisor in the dashboard reports the same problem as "RLS disabled in public".
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;Turn on RLS for each table, then add one policy per operation. Scope policies to the authenticated role and compare the row's owner column to auth.uid(). Inserts and updates need with check so users can't write rows that belong to someone else.
alter table public.profiles enable row level security;
create policy "Users read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);Replace placeholder policies like using (true) with real conditions. Don't base authorization on user_metadata — users can edit it themselves; use app_metadata or a roles table instead. Views bypass RLS unless created with security_invoker = true, and storage buckets need their own policies on storage.objects.
Query your API exactly the way an attacker would: with only the public key and no user session. If this returns rows that aren't meant to be public, your policies are too open.
curl "https://<project>.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"Use the service_role (sb_secret_) key only in server code such as API routes or Edge Functions, never behind a NEXT_PUBLIC_ or VITE_ prefix. If it has ever shipped to the browser, rotate it in the dashboard.
FAQ
Is it safe that my Supabase anon key is visible in the browser?
Yes — the anon (publishable) key is designed to be public, but only if every exposed table has RLS enabled with correct policies. The key itself grants nothing that your policies don't allow.
I enabled RLS and now my app returns empty results. Is it broken?
That's RLS working as intended: with no policies, every row is denied. Add policies for the operations your app needs, scoped to the right users, rather than switching RLS off again.
Does AppSafe test my RLS policies?
AppSafe doesn't log in to your database, so it can't evaluate your policies — use the SQL and curl checks above. It does scan your JavaScript for Supabase keys. If one is reported, decode the middle part of the JWT and check the role claim: anon is expected in the browser, service_role is a critical leak.
Is your app affected?
AppSafe checks for this and dozens of other issues in one free scan.
Scan my app freeRelated guides
Firebase Security Rules: stop leaving your database open
Your Firebase config is public by design. Your rules decide who sees the data.
API keys exposed in client-side JavaScript
Anything in your bundle is public. Treat it that way.
Exposed database backups: what happens when /backup.sql is reachable
A reachable .sql dump is a complete hand-off of your entire database.