backend · July 10, 2026 · 6 min read
Supabase Security: RLS, Anon Keys, and the service_role Mistake
Your Supabase anon key is meant to be public — RLS is the real gate. Learn the two mistakes that expose your entire database and how to fix them.
Supabase gives every project a Postgres database with a public REST API in front of it. That design is safe — but only if you understand which key does what, and only if Row Level Security is actually turned on. Most Supabase incidents come down to the same two mistakes, and both take minutes to check.
How Supabase security actually works
Three ideas, in plain words:
- The anon key is meant to be public. It ships in your frontend and identifies your project. It is not a password.
- The real gate is Row Level Security (RLS): rules attached to each table that decide which rows a given request may read or change.
- Your tables are reachable directly at
https://YOUR_PROJECT.supabase.co/rest/v1/<table>— with RLS on and policies written, the anon key gets exactly what you allow; with RLS off, it gets everything.
So “anon key visible in my code” is fine. “Anon key visible and RLS off” means your database is an open API.
Mistake #1: shipping the service_role key to the browser
The service_rolekey bypasses RLS entirely. It’s the admin key, built for trusted server code. If it ends up in your frontend JavaScript, anyone who views your bundle can read, modify, or delete every row in every table — policies don’t apply to it.
It usually gets there through one env-var mixup: the wrong key pasted into the client setup, or a build error “fixed” by slapping NEXT_PUBLIC_ onto a server variable. We cover that whole failure class in API keys in your JavaScript bundle. The same key also lives in .env files — if that file is web-reachable, the outcome is identical: exposed .env and .git files.
Mistake #2: leaving RLS off
Tables created through raw SQL don’t get RLS automatically. A table with RLS off is fully readable and writable by anyone holding the anon key — which, as we established, is everyone.
The flip side trips people up too: enabling RLS with zero policies blocks all access through the anon key. That’s by design. RLS is deny-by-default; policies grant access back, deliberately, one operation at a time.
Don’t forget storage buckets
Public buckets are fine for logos and avatars. They are not fine for user uploads, invoices, or exports. A common pattern in generated apps: every upload lands in one public bucket, and every file URL works for everyone, forever. Private buckets plus storage policies fix this.
Why AI code generators get this wrong
Tools like Cursor, Lovable, Bolt, and v0 optimize for a working demo. The fastest route to “working” is RLS off or service_role everywhere — no policy errors, no denied requests. And here’s the trap: the app behaves identically either way when you’re the only user. The difference only appears when a stranger queries your API directly, which is exactly what automated scanners do. That’s why Supabase configuration sits near the top of our checklist for AI-built apps.
How to check your project
1. In the Supabase dashboard. Open Table Editor (or Database → Tables) and check the RLS state on every table — Supabase flags unprotected tables prominently. Then check Settings → API and confirm which key your frontend code actually uses.
2. Test the API like a stranger. From a terminal, using only your public anon key:
# supabase-public-headers.txt contains only your project's public anon-key headers
curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" \
--config supabase-public-headers.txtRun this against each table while logged out of everything. Rows you didn’t expect to see means that table has an RLS problem.
3. Search your shipped JavaScript for service_role. The key is a token whose payload literally contains the string service_role — if it appears anywhere in your built frontend, rotate it now.
How to fix it
Enable RLS on every table and write explicit policies:
alter table public.profiles enable row level security;
create policy "Users can read their own profile"
on public.profiles for select
using ( auth.uid() = id );
create policy "Users can update their own profile"
on public.profiles for update
using ( auth.uid() = id );- Keep the service_role key in server-side environment variables only. If it ever shipped to a browser, rotate it in the dashboard immediately and assume the data was readable in the meantime.
- Make buckets private unless their contents are genuinely public, and add storage policies per operation.
- Re-run the curl test after every change — trust the live behavior, not the settings page.
Firebase has the same failure mode
Different product, same class of bug. Firebase’s web API key is public by design, and the real gate is security rules. Open Realtime Database or Firestore rules are the RLS-off of the Firebase world, and open Storage rules mirror the public-bucket mistake. If you’re on Firebase, run the same “test as a stranger” exercise against your data — and yes, we scan for both.
How BoringSec checks this
BoringSec’s Supabase scanner detects that your site uses Supabase, flags a service_role key in frontend code as critical, and — instead of guessing — live-checks whether your tables are actually readable without auth. It also checks your storage buckets. The Firebase scanner does the same for Realtime Database, Firestore, and Storage rules.
These results feed the Boring Score, our score for AI-built apps, weighted by real-world impact: secrets 40%, database 25%, auth 20%, headers 15%. Like all 24 of our scanner modules, it’s evidence-first — a critical finding needs captured proof or it’s downgraded to medium — and any confirmed critical caps your Security Score at 30. The full scoring model is on /methodology.
FAQ
Is it safe that my Supabase anon key is visible in my site's code?
My service_role key was in the browser. What now?
Does enabling RLS break my app?
Keep reading
Check your site now
One scan applies the relevant checks from 20 base URL modules plus 3 verified-owner background engines. Free, no signup, with base results appearing first.
Scan your site free