58 of 154 public Supabase apps had a working service_role key in git. Here's what that means for yours.
If you build with Lovable, Bolt, Cursor, or any AI coding tool on top of Supabase, this week's scan has a number you should see: 58 of 154 public apps shipped a working service_role key inside their git history.
That key is the master key for the database. It bypasses Row-Level Security entirely — the same RLS you turned on in the dashboard does not apply to it. Anyone who finds it can run DELETE FROM "user" over the REST API and the database believes it.
I did not read a single row of anyone's data. I verified the key decodes to a real project (iss, ref, and a service_role role claim), checked that the project is live, and moved on. That is enough to know the exposure is real.
Why this keeps happening (it is not laziness)
Three causes, in the order I actually saw them:
AI tools scaffold
.envfiles with real keys. Your AI agent opens the Supabase dashboard to "wire up the database," copies the project URL and keys into.env, and commits it because the app "won't build" otherwise. The placeholders in memory get replaced by real values — then.gitignoreis never created for.env.The file that ships is named like a dev file.
.env.txt,env.local,.env.development— these often bypass the.gitignorepattern*.envbecause the name is subtly different.History keeps the key after the file is deleted. Deleting
.envin a new commit is not a fix. The key stays in every previous commit unless you rewrite history.
The 10-second check for your own repo
Paste this into supabase.com/dashboard → your project → SQL Editor. It shows every object a service_role (or anon) key can reach that your RLS policies do not actually protect:
select n.nspname, c.relname, c.relrowsecurity
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind in ('r', 'p')
and n.nspname = 'public'
and not c.relrowsecurity
order by n.nspname, c.relname;
Every row of that result is readable and writable by anyone holding the public anon key that ships in your browser bundle. If relrowsecurity is false, Row-Level Security is not even on for that table.
For git: git log --all --oneline -- .env shows every commit the file touched. If you see more than the one "deleting it" commit, the key is still in history.
If you find one — do this first
- Rotate the key first (dashboard → Settings → API), then rewrite history (
git filter-repoor BFG). - Fix the commit habit that caused it: one line in your AI tool's instructions — "never commit
.env, create.gitignoreentry*.env*before first launch." - Re-run the check above on the same project to make sure nothing else is open.
Every check in that list — RLS coverage, open policies, USING (true) traps, write-without-read policy conflicts, and the git-history secret scan — I've packaged as a free, read-only, 60-point pre-launch check that runs against your own DB in about 30 seconds and uploads nothing:
github.com/cekuu35/supabase-rls-leak-demo
The scan behind this post ran the same evidence standard: decode the JWT, confirm role == "service_role", confirm the project is live. No data was read, no exports were made, nothing was saved. If you want a second pair of eyes on your schema after rotating — policy conflicts, unisolated joins, service_role exposure paths — that is the read-only review I do as paid work, and the email + Gumroad link are in my profile.
Top comments (0)