Why this happens
Supabase's docs describe the publishable key as "taped to the front door": it only opens what RLS allows. With the key, anyone can call your project's REST API directly, bypassing your UI entirely. So your front-end checks ("only show the admin button to admins") protect nothing. The database policies are the real lock.
This went wrong at scale with AI-built apps. In CVE-2025-48757, researchers scanned the homepages of 1,645 Lovable showcase projects and found 170 with inadequate RLS. Exposed data included user tables, subscriptions and stored API keys, and in one case they could write payment_status: "paid" without paying.
Common gaps:
- RLS disabled on a table created by a migration.
using (true)select policies on private data.- Policies only for SELECT. Missing insert, update or delete policies, or
with check (true). - "Any logged-in user" policies (
to authenticated using (true)) on per-user data. - Views or
security definerfunctions that skip RLS.
How to fix it, step by step
1. Find tables without RLS:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;Every row should say true. Enable it where it doesn't: alter table public.<table> enable row level security;
2. Review every policy:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;Look for qual = true, with_check = true, policies for anon on private tables, and tables missing UPDATE or DELETE rules.
3. Use owner-scoped policies (assuming a user_id column):
create policy "own rows: select" on public.notes for select to authenticated
using ((select auth.uid()) = user_id);
create policy "own rows: insert" on public.notes for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "own rows: update" on public.notes for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "own rows: delete" on public.notes for delete to authenticated
using ((select auth.uid()) = user_id);4. Never let users set sensitive fields. Columns like role, plan or payment_status should only be written by server code (webhooks, Edge Functions with the secret key), not through an update policy users can use.
5. Check views and functions. Create views with with (security_invoker = true) (Postgres 15+), and review any security definer function exposed through RPC.
6. Test as an outsider. With only the public key, try reading a private table:
curl "https://<ref>.supabase.co/rest/v1/notes?select=*&limit=1" \
-H "apikey: <publishable-or-anon-key>"You should get [] or a permission error, never someone's data. Then run Advisors → Security Advisor in the dashboard.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Claude, ChatGPT, Replit, v0 or whichever tool you build with. Replace the bracketed parts first.
Audit our Supabase Row Level Security. Our public key is in the browser, so RLS is the only thing protecting data. Please:
1. List every table in the public schema and say whether RLS is enabled. Write a migration enabling it on any table where it's off.
2. For each table, write policies for select, insert, update and delete for the authenticated role, scoped to (select auth.uid()) = user_id (or the right owner column). Remove any using (true) or with check (true) policies on private data.
3. Make sure columns like role, plan, is_admin and payment_status can't be updated by users. Only server code may change them.
4. Flag any views without security_invoker and any security definer RPC functions.
Don't disable RLS anywhere and don't move the secret or service_role key into the front end.How to catch this next time
RLS regresses quietly. A new table created by an AI edit starts without the policies you carefully wrote for the others.
Heedza's passive security scan reads your homepage, its inline scripts and up to six same-origin JavaScript bundles. It tells you when Supabase config and a public key (legacy anon or new sb_publishable_) are visible, which is expected. It raises a high alert if a service_role key is there and a critical one for an sb_secret_ key. If you verify you own the domain (DNS TXT record or meta tag), Heedza also makes single reads with that key against up to three tables listed in your API schema and flags any that return rows. It doesn't test inserts, updates or deletes, so the SQL review above still matters. Start with the free scan of your live URL.