Supabase anon key exposed: is it a problem?
Short answer: the anon key being visible is normal. Whether that's a problem depends entirely on your Row Level Security. The service_role key is a different story.
Security scanners, GitHub alerts and worried friends regularly flag Supabase keys in frontend code. Sometimes that's a real incident. Often it's expected. Here's how to tell the difference in a few minutes.
Know which key you're looking at
Supabase projects have two kinds of API keys, and they mean completely different things:
- Anon / publishable key — meant for the browser and mobile apps. Requests made with it run as the
anonrole (or as the signed-in user once they log in), and Row Level Security applies. - service_role / secret key — meant only for trusted servers. It bypasses Row Level Security entirely.
Older projects use JWT-format keys for both (long strings starting with eyJ). Supabase has also introduced newer key formats: publishable keys starting with sb_publishable_ and secret keys starting with sb_secret_. Check your project's API settings to see which your project uses.
To identify a legacy JWT key, decode its middle section (the payload) at any JWT decoder or in your browser console with JSON.parse(atob(key.split('.')[1])). Look at the role claim: anon or service_role. Don't paste a service_role key into third-party websites. If you suspect it's service_role, decode it locally.
The free Last Mile pre-launch check does this decode for you on a public URL. It marks an anon key as info and a service_role key as a fail, and shows both masked.
Case 1: anon key in the browser — expected
If your app talks to Supabase from the client, the anon key must be in the bundle. Anyone can extract it in seconds. Hiding or obfuscating it doesn't work and isn't needed.
What matters is what someone can do with it. With the anon key and your project URL, anyone can call your Supabase REST and GraphQL APIs directly, skipping your UI entirely. So the real question is:
If a stranger calls every table directly with the anon key, what do they get?
With RLS enabled and correct policies: only rows your policies allow for an anonymous or signed-in user. With RLS disabled on a table: possibly everything in it, depending on your grants.
Check your exposure in five minutes
- Dashboard check. Supabase dashboard → Table Editor. Every table in exposed schemas (usually
public) should show RLS enabled. The dashboard's Security Advisor also flags tables without RLS. - Policy read. For each table, read the policies. "Enable read access for all users" on a table of private data is a leak with extra steps.
- Direct call test. From a terminal or the browser console, request a table with only the anon key, no user session. For example:
fetch('https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*', {headers: {apikey: ANON, Authorization: 'Bearer ' + ANON}}). You should get an empty list or an error for private tables, not rows. - Two-user test. Signed in as user A, try to read or update user B's rows by ID. It must fail.
Case 2: service_role or secret key in the browser — incident
If the key in your bundle is service_role (or an sb_secret_ key), treat it as a breach:
- Rotate it now in the Supabase dashboard (API settings). Read the current docs for your key type first, since rotating can break server code that uses the old key.
- Remove it from client code, environment variables with public prefixes (
NEXT_PUBLIC_,VITE_), and any git history you control. - Move privileged operations to the server (edge function, API route) and have that code verify the caller and their permissions.
- Review data and logs for the period the key was public. Check for unexpected reads, writes, new users or deleted rows.
- Consider notification obligations if personal data could have been accessed. This depends on your jurisdiction and isn't something a checklist can settle.
Common anon-key mistakes
- RLS enabled, policy too broad.
using (true)or "any authenticated user" on per-user tables. - Insert policies that trust the client. No
with check (auth.uid() = user_id), so users can create rows as someone else. - Views and functions. A view or security-definer function can expose rows that RLS would hide. Review anything in exposed schemas.
- Storage. Buckets have their own policies. A public bucket is public to anyone with the URL.
- Treating the anon key as the problem. Rotating an anon key doesn't fix open tables. The next build will ship the new key, and the tables are still open.
The project URL is public too
Your https://xyz.supabase.co URL sits next to the anon key in the bundle. That's also normal. It tells an attacker exactly where to send requests, so the same answer applies: policies decide what they get.
Bottom line
An exposed anon key is a prompt to verify RLS, not an incident. An exposed service_role key is an incident. Know which one you have, then check policies with a direct call and a two-user test.
More: the Supabase RLS checklist, the free launch checklist preview, and the full Last Mile kit with prompts P05 and P06 for ownership and RLS audits.
Related guides
- Supabase RLS checklist before launch
If your vibe-coded app talks to Supabase from the browser, Row Level Security is the difference between “users see their own data” and “anyone with the anon key reads the whole table.”
- Leaked API keys in your frontend bundle: how to check
If a secret can call a paid API or bypass your database rules, it does not belong in JavaScript the browser downloads. Here is how to find leaks fast and what to do when you find one.
- Lovable and Bolt app security before launch
AI app builders get you from idea to working app in an afternoon. They don't decide who should be allowed to read which rows. That part is still yours, and it's checkable without writing code.
Get launch notes
$ subscribe --launch-notes # new checks, prompts and shipping tips for vibe coders. No spam.