Guide
Is the Supabase anon key safe to expose?
Yes — and hiding it would break your app. There is exactly one condition attached, and it is the part everyone skips.
The anon key is built to be public. It ships in the JavaScript of every Supabase app on purpose. It is safe on one condition: row-level security has to be on, with a policy on every table that holds private data.
Without that, the public key reads every row of a table rather than just the current user's. The key was never the problem. The missing policy is.
eyJ and look nearly identical at a glance. One word in the middle is the entire difference.Why the key is public in the first place
Supabase gives your project two keys. The anon key is an identifier: it says which project a request belongs to, and nothing more. It carries no privileges of its own, and it has to be in the browser, because the browser is what talks to your database.
So finding it in your bundle is not a leak. It is the design working. Anyone telling you to hide it has misunderstood what it does.
What the key can actually do is decided somewhere else entirely.
The key that is genuinely dangerous
The service_role key is the opposite in every way. It bypasses row-level security completely — reads, changes and deletes any row in any table, ignoring every policy you have written.
It belongs on your server and nowhere else. If it is in your client bundle, treat it as already compromised, because it is public and has been for as long as the site has been live.
How to tell which one you shipped
- Open your live site, right click, view source.
- Search the page and its JavaScript for
eyJ. - Copy the whole string — three parts, separated by dots.
- Base64-decode the middle part. Any decoder works, or run
atob()in the console. - Read the
rolefield.
"role":"anon" is expected and fine. "role":"service_role" means stop and rotate it in the Supabase dashboard now, then find and remove it from your client code.
Checking row-level security properly
Open the Table Editor in your Supabase dashboard. Tables without RLS carry a warning. That is step one, but not the whole job — there are two ways to get this wrong, in opposite directions:
- RLS on, no policies. Everything is denied. Your app breaks, you notice immediately, you fix it.
- RLS on, with
USING (true). Everything is allowed. Your app works perfectly, you notice nothing, the table is world-readable.
The second is the dangerous one, because nothing about it feels wrong. What you want on user data is a policy scoping rows to their owner:
create policy "own rows" on public.your_table for select using (auth.uid() = user_id);
Then repeat for every table holding anything private. One unprotected table is enough.
What can and cannot be checked from outside
Anyone can see which key your app ships, including us — it is in a public file. Whether the tables behind it are locked down is a different question, and answering it means sending a query to your database.
On a site we cannot prove you own, we do not send that query. That is why our report says a database may be readable rather than asserting it is. A scanner that tells you your tables are definitely readable, without you having proved ownership, queried a database it had no permission to touch.
Frequently asked
Should I rotate my anon key?
Not on its own. Rotating it breaks your app until you update it and fixes nothing, because the anon key was never the weakness. Fix the policies instead.
Can I restrict the anon key by domain?
No, and this is the common wish. A key in client-side JavaScript can be copied and used from anywhere — curl, a script, another site. Access control has to live in the database, which is what row-level security is.
My app works fine. Doesn't that mean it is configured correctly?
No. An app with no policies works perfectly for its users, often better, because nothing is being filtered. Working and secure are unrelated properties here, which is exactly why this gets missed.
What if the service_role key has been public for months?
Rotate it, then check your Supabase logs for requests you cannot account for. Treat the data it could reach as potentially accessed. Rotating stops it continuing; it does not undo what came before.