Supabase RLS mistakes that leak your data
Your Supabase anon key ships in your frontend. That's by design. What keeps strangers out of your tables is Supabase RLS (Row Level Security) plus your grants, and AI-built apps often get one of them wrong. Here are the four mistakes we see most, how to spot them, and SQL you can paste.
QUICK CHECKLIST
- RLS enabled on every table in public
- No write policy that allows everyone (true)
- Grants to anon revoked where not needed
- Service role key only on the server
- Tested with the anon key while logged out
Why Supabase RLS matters
Supabase's docs say that a table in an exposed schema without RLS is readable and writable by any role that has a grant on it. In March 2025, researcher Matt Palmer scanned 1,645 apps built with Lovable and found 303 exposed endpoints across 170 of them, about 10.3%. The issue became CVE-2025-48757. Lovable disputes the CVE, saying each customer is responsible for protecting their own app's data. Either way, the data in those tables was reachable.
Mistake 1: tables created in SQL without RLS
Tables you make in the Supabase Table Editor get RLS on by default. Tables created with SQL, in the SQL editor or in a migration file, don't. If your AI tool wrote the schema as a migration file, check every create table in supabase/migrations for a matching line:
alter table public.todos enable row level security;
Mistake 2: policies that say using (true) on writes
RLS is on, but the policy lets anyone update or delete any row. for update to authenticated using (true) still means every signed-up user can edit every row. Scope writes to the owner:
create policy "update" on public.todos for update to authenticated using (true);
create policy "own rows: update" on public.todos for update to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
The same goes for auth.role() = 'authenticated' on deletes. See why any signed-in user can delete your data.
Mistake 3: forgetting grants
Policies don't remove grants. Supabase's docs note that on existing projects a new table in public starts with select, insert, update and delete granted to anon and authenticated. Revoke what signed-out visitors don't need: revoke all on table public.todos from anon;
Mistake 4: the service role key in the browser
The service role (secret) key bypasses RLS completely. If it ends up in client code or a NEXT_PUBLIC_ variable, your policies don't matter. Keep it on the server only and rotate it if it ever shipped.
How to check your Supabase RLS
List tables without RLS
Run this in the SQL editor. Every row with
falseneeds attention:select tablename, rowsecurity from pg_tables where schemaname = 'public';
Enable RLS
For each of them:
alter table public.<name> enable row level security;Read the policies
List them with
select * from pg_policies where schemaname = 'public';and look fortruein insert, update and delete policies. Views and functions can skip these policies, see security definer views.Revoke grants anon doesn't need
Signed-out visitors usually need select on a few public tables at most.
Test while logged out
Call the REST API with only the anon key and a broad
select. Supabase's Security Advisor in the dashboard also flags tables inpublicwithout RLS.
Fix prompt you can paste
Go through every table in supabase/migrations. For each one: make sure RLS is enabled, replace any policy that uses (true) for insert/update/delete with an ownership check on auth.uid(), revoke grants anon doesn't need, and write a new migration with the changes. List any table that should stay public read-only.
How Leakcade checks this
Our auth-route check reads your SQL migrations as text. It flags tables that are created without enable row level security and policies that allow writes with (true). It can't see changes made only in the dashboard, so the report tells you when that's the likely explanation.
When we tested 13 public repos, this check flagged 12 open write policies in one Supabase blog starter and one table without RLS in another.
Further reading
- Supabase docs: Row Level Security
- Supabase docs: Securing your API
- Matt Palmer: Statement on CVE-2025-48757
- NVD: CVE-2025-48757
FAQ
Is the Supabase anon key secret?
No. It's meant to be public and ships in your frontend. It can only reach what your grants and RLS policies allow, which is why RLS matters.
What happens if RLS is on but there are no policies?
Requests with the anon or authenticated role get no rows back, inserts are rejected, and updates and deletes affect no rows. The table is locked until you add a policy. The service role still has full access.
Does the service role key bypass RLS?
Yes. It ignores every policy, so it belongs only in server code, never in the browser or a public env variable.
Do views respect RLS?
Not by default. A view runs with the rights of the user who created it, which can bypass RLS on the tables underneath. On Postgres 15 and later, create it with security_invoker = true so the caller's policies apply.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms