Is the Supabase anon key safe to expose in the browser?
Why the Supabase anon or publishable key is meant to be public, what it can reach when RLS is missing, and how to check your own project in ten minutes.
Leer en español →
Yes, the Supabase anon key (now called the publishable key, starting with sb_publishable_) is designed to sit in your browser code, and anyone can read it. It is only safe because Row Level Security (RLS) decides what that key can see and change. If a table in an exposed schema has RLS turned off, or has a policy that says "allow everyone", the public key gives the whole internet that access.
This guide explains what the key actually is, what it can and can't reach, the four places AI-built apps usually leak data through it, and a short test you can run on your own project today.
What is the anon (publishable) key, exactly?
Supabase gives every project two kinds of keys. According to the Supabase API keys docs, they are:
| Key | Looks like | Meant for | Respects RLS? |
|---|---|---|---|
| Publishable | sb_publishable_... | Browsers, mobile apps, anything public | Yes |
| Secret | sb_secret_... | Servers and Edge Functions only | No, it bypasses RLS |
anon (legacy) | Long JWT starting with eyJ | Same as publishable | Yes |
service_role (legacy) | Long JWT starting with eyJ | Same as secret | No |
The publishable key identifies your project. It is not a password. Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026, so if your Lovable, Bolt or Cursor project still has a long eyJ... key in its environment, plan the switch soon.
When a visitor is not signed in, requests made with the publishable key run as the Postgres role anon. When they sign in, the same key plus their session token runs as authenticated. Your policies decide what each role may do, row by row.
So when is the public key dangerous?
The key itself never is. What is dangerous is a database that trusts it. These are the four ways it usually happens in apps generated by AI tools.
1. A table with RLS switched off
If RLS is off on a table in the public schema, and the table has the usual grants, anyone with your publishable key can read and often write every row through the REST API. Generated apps often create a table in a later prompt, after the first round of security work, and that new table never gets RLS.
2. A policy that allows everything
Policies like using (true) for select, or an insert policy with with check (true), look like a fix because the "permission denied" error goes away. They also remove the protection. A true policy for reading is fine for truly public content such as published blog posts. It is not fine for orders, messages or profiles with emails.
3. Views and functions that skip RLS
A Postgres view runs with the permissions of its owner by default, which in Supabase is usually a role that bypasses RLS. If you create a view over a protected table, it can leak rows unless you create it with security_invoker = true:
create view public.my_orders
with (security_invoker = true)
as select id, total, created_at from public.orders;
Likewise, a function marked security definer in an exposed schema can be called by anyone through RPC and runs with its owner's rights. The Supabase RLS guide says not to create security definer functions in exposed schemas at all.
4. Storage buckets set to public
A public Storage bucket serves files to anyone who knows the URL, no key needed. That is right for logos and product photos and wrong for invoices or ID scans. We cover that in public vs private buckets in Supabase Storage.
How do I check whether my key is exposing data?
You don't need to read the code to get a first answer. Do this with a test project or your own data only.
- Open the Security Advisor in your Supabase dashboard. It flags tables in exposed schemas without RLS and some risky functions. Fix every item it marks as an error before anything else.
List your tables and their RLS status. In the SQL editor:
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by tablename;Every row should show
true.Try reading a table as an anonymous visitor. Copy your project URL and publishable key from the Connect dialog, then:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \ -H "apikey: sb_publishable_xxx"If you get back rows you didn't mean to make public, that table needs a policy fix. An empty array
[]usually means RLS is on and no policy letsanonread.- Repeat as a signed-in user, but one who should not see the data, such as a second test account. Many leaks only show up for logged-in users, because policies were written as "any authenticated user" instead of "the owner".
- Read every policy that contains
trueand decide whether that table really is public.
What changes with the new Data API grants?
Supabase is also changing what new tables expose by default. Since May 30, 2026, new projects no longer grant the anon and authenticated roles access to new tables in public automatically, and Supabase's changelog says the same default reaches existing projects on October 30, 2026. Existing tables keep their current grants.
In practice this means a table created after that date returns permission denied for table ... (code 42501) through the API until someone runs an explicit grant. That is safer, but it tempts people to paste a broad grant and move on. A grant opens the door to the table; RLS still decides which rows come through it. You need both.
What should the secret key be used for instead?
Anything that must ignore RLS on purpose: a Stripe webhook that marks an order as paid, a nightly job, an admin script. Those run on a server or in an Edge Function, never in the browser. Supabase blocks secret keys in browsers based on the request's User-Agent, but that does not protect a leaked key used from a script. If you think a secret key has been shipped to the front end, treat it as leaked and follow what to do when the service_role key leaks.
A quick rule for environment variables: anything prefixed with VITE_, NEXT_PUBLIC_ or EXPO_PUBLIC_ ends up in the bundle that visitors download. The publishable key may live there. The secret key must not.
Quick answers
Should I hide the anon key with an environment variable? You can keep it in an environment variable for convenience, but it will still be visible in the browser. Hiding it is not a security measure; RLS is.
Is the publishable key safer than the old anon key? It has the same level of access. Its advantages are that it is easier to rotate and that it isn't a long-lived JWT. Your policies matter just as much.
Can someone delete my data with the anon key? Only if a policy allows delete (or update) for that role, or RLS is off on that table. With correct policies, no.
Does turning on RLS break my app? It can, briefly. With RLS on and no policies, nothing is returned. Add a policy for each thing the app legitimately needs. Our RLS policy examples cover the common patterns.
Want a second pair of eyes on it?
The free App Health Check shows what is visible from outside your app, including keys in the bundle. If you'd like someone to go through every table, view, function and bucket, our Supabase security review does exactly that, and the security checklist for AI-built apps is a good place to start on your own. Questions are welcome through contact.


