28 October 2026 · 6 min read · BlitzHash Team · AI & Vibe Coding
Is the Supabase anon key safe in the browser? Only if this is true
If you built your app with Lovable, Bolt, Cursor or another AI tool, you have probably noticed a Supabase key sitting in plain sight in your frontend code. Maybe a friend, a security scanner or a worried customer has asked the obvious question: isn't that a password anyone can copy?
The short answer is that the browser key is designed to be public. It is safe only if your database is set up to treat every visitor as a stranger. This guide explains what the key actually does, the conditions that make it safe, and how to check them in under an hour.
What the browser key is (and isn't)
Supabase's own documentation puts it simply: every request to your project carries an API key, and that key identifies *what* is calling your project, such as a web page, a mobile app or a server. It does not identify *who* is calling. That is the job of Supabase Auth, which gives each signed-in user their own token.
There are two families of keys you may see:
- Publishable key (starts with
sb_publishable_). This is the current key for anything you ship to users: web pages, mobile apps, scripts. Supabase describes it as safe to expose online. - Secret key (starts with
sb_secret_). This is for code you control, such as servers, Edge Functions and scheduled jobs. It gives full access to your data and bypasses Row Level Security.
Older projects use the legacy versions of these keys: anon (the old publishable key) and service_role (the old secret key). Both are long strings that begin with eyJ. Supabase says it is deprecating the anon and service_role keys by the end of 2026, so if an AI assistant or an old tutorial tells you to paste an eyJ... key, it was written for the legacy system.
Supabase's docs use a good picture for this: the publishable key is taped to the front door and only opens the lobby, while the secret key is the master key that stays in your pocket.
Why "public" doesn't mean "open"
When your app sends a request with the publishable key, Supabase maps it to a Postgres role:
| Key used | User signed in? | Postgres role | |---|---|---| | Publishable | No | anon | | Publishable | Yes | authenticated | | Secret | Not applicable | service_role |
Your Row Level Security (RLS) policies decide what the anon and authenticated roles may read and write. So the real security boundary is not the key. It is the rules on each table.
That is the part AI-built apps most often get wrong. The key is public no matter what you do, since anyone can open the browser's network inspector and copy it. If a table has RLS turned off, or has a policy that says "anyone can read everything", the key works exactly as designed and hands that data to whoever asks.
The four conditions that make the browser key safe
1. RLS is enabled on every table in an exposed schema
Supabase's security guidance says to confirm that Row Level Security is enabled on all tables. Tables created through raw SQL or by an AI agent sometimes skip this step. One table without RLS, such as profiles, orders or messages, is enough to leak data.
How to check: open the Security Advisor in your Supabase dashboard. It flags tables in public schemas that have RLS disabled, along with other common problems with the built-in roles. Treat every finding as real until you have read it.
2. Policies for anon and authenticated are deliberate
RLS that is enabled but has a policy like using (true) for every role is the same as no RLS at all. Review each policy and ask two questions:
- Should someone who is not signed in see this row at all?
- Should a signed-in user see only their own rows, or their team's rows?
A typical safe pattern for user-owned data is a policy that compares the row's owner column with auth.uid(). For shared data, such as a team workspace, the policy should check membership in a separate table, not trust a value the browser sends.
3. The secret key never reaches the browser
The publishable key is only safe if it is the only key in your frontend. Search your built JavaScript and your repository for sb_secret_ and for the legacy service_role key. Watch for environment variables with a public prefix, such as NEXT_PUBLIC_ or VITE_. Supabase's own example warns that you should never put a public prefix on a server-only key, because your bundler will ship it.
The newer secret keys add a guardrail here: Supabase blocks them when they are used from a browser, based on the request's User-Agent, and returns a 401 error. That does not make a leaked secret key harmless. An attacker can still use it from other tools. If one leaks, delete it straight away and create a new one.
4. Grants and roles haven't been changed carelessly
Supabase also asks you not to change the attributes of the built-in roles unless you understand what the change does. AI agents sometimes "fix" a permission error by granting broad rights to anon. Postgres checks table grants first and only then applies RLS, so a missing grant gives a permission error while a policy that matches nothing gives an empty result. Fix the right layer instead of widening access to make an error go away.
What the publishable key can't protect you from
Supabase is explicit that publishable keys are not meant to protect against reverse engineering, people reading network traffic in the browser, cross-site scripting, cross-site request forgery or phishing. Those are application security problems. RLS limits the damage, because even a hijacked session can only reach what that user is allowed to reach, but you still need sensible frontend practices: escape user content, keep dependencies patched and avoid storing sensitive data you don't need.
A one-hour self-check
1. List your tables. In the dashboard, look at every table in the public schema and any other schema your API exposes. 2. Run the Security Advisor. Fix every "RLS disabled" finding first. 3. Read each policy. Write down, in plain words, who can read and write each table. If you can't explain a policy, it needs review. 4. Test as a stranger. In a private browser window, sign out and try to load pages that should be private. Use the network tab to see what the API returns. 5. Test as the wrong user. Create two test accounts. Sign in as one and try to load the other's records by changing an ID in the URL or the request. 6. Search for secret keys. Search your repository, your deployed bundle and your hosting environment variables for sb_secret_, service_role and any variable with a public prefix that holds a secret. 7. Plan the key migration. If you still use legacy keys, create a publishable and a secret key, switch your app over, and then disable the legacy keys in Settings > API Keys. Creating new keys doesn't revoke the old ones; disabling them is a separate step.
Practical takeaways
- The browser key is public by design. Your RLS policies are the real lock.
- One table without RLS, or with a "read everything" policy, can expose all of its rows.
- Secret keys belong only on servers and Edge Functions you control.
- Move off the legacy
anonandservice_rolekeys before Supabase retires them at the end of 2026. - Test with a signed-out window and with two accounts. It's the fastest way to see what a stranger can see.
For a broader list, see our production checks for AI-built apps.
---
Not sure whether your tables are really locked down? Our Supabase RLS check reviews every table, policy and key in your project. Book a free technical assessment, and we'll tell you what a stranger can see today and what to fix first.
