Rustine

Supabase Anon Key: Is It Safe to Expose in the Browser?

Yes. The Supabase anon key is safe to expose in the browser — that's exactly what it's for. But "safe" comes with one condition that most AI-built apps miss: the anon key only protects what your Row Level Security (RLS) policies allow. Turn RLS off, and anyone holding that key can read your entire database through the public API.

A browser window showing a Supabase API key beside a padlock and a database table with row-level access rules, some rows locked and some open.
Contents
  1. What is the Supabase anon key?
  2. Is the anon key safe to expose?
  3. What can someone actually do with your anon key?
  4. Anon key vs service role key
  5. The anon key is being retired: meet publishable and secret keys
  6. How to check your own app before launch
  7. Frequently asked questions
  8. Where do I get my Supabase anon key?
  9. What is an anon key in Supabase?
  10. What is the secret key in Supabase?
  11. Do I need to rotate the anon key if it was exposed?
  12. Is RLS on by default in Supabase?
  13. Is the anon key the same as the publishable key?
  14. Sources

One more thing before you paste anything: Supabase is retiring the anon key by the end of 2026 and replacing it with a new publishable key. If an AI assistant or an old tutorial tells you to copy a long key that starts with eyJ, it's teaching you the legacy setup. This guide covers the key you have today, the key you'll migrate to, and the exact check that decides whether your app is actually protected.

A laptop showing lines of code in a dark editor Photo by Caspar Camille Rubin on Unsplash

What is the Supabase anon key?

Every request to your Supabase project carries an API key. The key identifies what is calling your project — a web page, a mobile app, a server, or an Edge Function. It does not identify who. Identifying the user is a separate job handled by Supabase Auth, which issues each signed-in user their own JWT.

The anon key is the key you use in anything you ship: browser code, a mobile app, a CLI, a public script. It maps to the Postgres role anon when nobody is signed in, and to authenticated when someone is. "Anon" is short for anonymous — the lowest-privilege role in the system.

A useful way to picture it, straight from Supabase's own docs: your data is a building. The anon key is taped to the front door. It opens the lobby and nothing else. The service role key is the master key that stays in your pocket.

The important part of that analogy: what the key opens depends on the building's interior doors — your RLS policies — not on the key itself.

Is the anon key safe to expose?

Yes, with RLS on.

The anon key is not a secret. It's designed to be committed, bundled, and read by anyone who opens your app in DevTools. Supabase is explicit about this: a publishable or anon key "can identify your project, but it is not a secret."

What makes it safe or unsafe is not the key. It's the policy layer behind it. When RLS is enabled on a table, the database checks every query against your policies before returning rows. With RLS off, a table is open — and the anon key is all anyone needs to select from it (and, depending on grants, write to it) through the public Data API.

So the honest answer has two halves:

  • The anon key itself: safe to expose, always.
  • Your database with an anon key and no RLS: fully exposed.

This is why a leaked anon key is almost never the incident. The incident is a table with RLS disabled that the app creator assumed was private.

What can someone actually do with your anon key?

Let's be concrete. Someone who copies your anon key from the browser can call your Supabase REST endpoint directly — no client library needed, just a URL and the key. The anon key is public by design, so there's nothing to "hack" to get it.

What they can read or change depends entirely on your policies and grants:

  • If a table has RLS enabled and no permissive policy, they get an empty result or a permission error.
  • If a table has RLS enabled with a policy that allows anon reads, they can read only what that policy returns.
  • If a table has RLS disabled, they can read every row and, depending on grants, write to it too.

Two details trip people up. First, Postgres evaluates table grants before RLS. A missing grant returns a permission error, while a policy that matches no rows returns an empty result. The two failures look different, so check the grant before you debug the policy.

Second, the anon key maps to anon only when nobody is signed in. The moment a user signs in, their requests use the authenticated role. "Public data" and "data any signed-in user can see" are two different policy decisions, and both matter.

Rows of data, some visible and some filtered out Photo by Mika Baumeister on Unsplash

Anon key vs service role key

This is the distinction that separates a safe app from a breach.

The service role key maps to the Postgres role service_role, which has the BYPASSRLS attribute. It skips every RLS policy you write. It has full access to your data, always, by design. It belongs only in code you control — servers, Edge Functions, cron jobs, admin scripts — and never in a browser, a shipped app, or source control.

The anon key maps to anon/authenticated and is filtered by RLS on every request.

The rule in one line: anon key in the client, service role key only on your own backend.

Here's the failure mode that bites vibe-coded apps. An AI assistant generates a page that reads from Supabase and, to "make it work," drops the service role key into the client. The app works. It also ships a key that bypasses every policy — and the first stranger who inspects the network tab gets your whole database.

If you've done this, the fix is not to rotate the key and hide it in a different file. The fix is to remove it from the client entirely and use the anon key with RLS. Rotate it anyway after you remove it: once a secret key has shipped to a browser, treat it as leaked.

The anon key is being retired: meet publishable and secret keys

Supabase is deprecating the anon and service_role keys by the end of 2026. New projects get publishable and secret keys instead. The mapping is direct:

  • anon → publishable key (sb_publishable_...): low privilege, safe to expose.
  • service_role → secret key (sb_secret_...): elevated, bypasses RLS, backend only.

The new keys are short strings, not JWTs. That's why a long key starting with eyJ is the giveaway that you're looking at a legacy key.

Secret keys add real protections the old service role key lacked. They return an HTTP 401 if used from a browser (Supabase matches on the User-Agent header), and you can create a separate secret key per service, so one leak only forces one rotation. Publishable keys carry the same low privileges as the anon key, so your RLS policies behave unchanged.

Migrating is deliberately low-risk: both key systems work at the same time. You create the new keys, swap them in client and server code, and disable the legacy keys only after nothing depends on them. For most vibe-coded apps the migration is two small edits: replace the anon key with sb_publishable_... in client code, and replace the service role key with sb_secret_... read from an environment variable in backend code.

A rack of servers in a server room Photo by Kevin Ache on Unsplash

How to check your own app before launch

You can verify all of this on your own app in about ten minutes. No third-party tool required for the first pass.

  1. Open the browser, load your app, and check the network tab. Find every request to *.supabase.co. The apikey header should be your anon or publishable key — never service_role or sb_secret_.
  2. Search your repo for service_role and sb_secret_. If either appears in a file that ships to the browser, that's a full database exposure.
  3. In the Supabase dashboard, open each table and confirm RLS is enabled. For every table your app reads or writes, there should be at least one policy for anon and authenticated that matches your app's intent.
  4. Check your Storage buckets too. A public bucket is a common leak, and it has its own separate visibility toggle.

Here's a prompt you can paste into Cursor, Claude Code, or Lovable to have your agent do the first pass and apply the fix:

text
Audit this project's Supabase usage for key leaks and missing Row Level Security.

1. Find every place that creates a Supabase client. For each one, list the file, the variable holding the key, and whether the file runs in the browser or on the server.
2. In client code, keep only the publishable key (starts with sb_publishable_) or the legacy anon key. Remove any key that starts with sb_secret_ or is a long JWT beginning with eyJ that mentions service_role.
3. In server code (API routes, Edge Functions, cron jobs), use the secret key (starts with sb_secret_) read from an environment variable — never hardcoded, never committed.
4. For every table the client reads or writes, enable Row Level Security and add the smallest policies for the anon and authenticated roles that still let the app work.

Then check that no "service_role" or "sb_secret_" string appears in any file that ships to the browser, and that every table in the dashboard shows RLS enabled.

This is exactly the class of problem Rustine scans for. Instead of eyeballing the network tab, a quick scan shows every request your app makes, flags the keys and tables that leak, and gives you the patch to paste back into your agent. If you build with Cursor or Claude Code, Rustine's MCP server can run the same scan and apply the fix by itself.

Frequently asked questions

Where do I get my Supabase anon key?

In the Supabase Dashboard, open Settings > API Keys, or use your project's Connect dialog. The legacy anon key and the new publishable key both appear there. If you're working locally, supabase start prints keys for your local project.

What is an anon key in Supabase?

An anon key is a public API key for the anonymous Postgres role. It identifies your project to the Supabase API and lets a request reach only what RLS allows for unauthenticated users. It is not a secret and is safe to ship in client code.

What is the secret key in Supabase?

The secret key (the successor to the service role key) is an elevated key that maps to the service_role role. It bypasses Row Level Security and has full access to your data. It must never leave your backend, your source control, or your client code.

Do I need to rotate the anon key if it was exposed?

No — the anon key is public by design, so exposure alone isn't a problem. Rotate it only if you want a clean break after a policy mistake. What you must rotate immediately is the service role or secret key if it ever appeared in client code or source control.

Is RLS on by default in Supabase?

Supabase recommends enabling RLS on every table, and the dashboard prompts you to do so, but tables created by raw SQL or an AI agent can land with RLS off. Don't assume — check each table in the dashboard rather than trusting the default.

Is the anon key the same as the publishable key?

Functionally, yes. The publishable key is the successor to the anon key: same low privilege, same role mapping, but a short string instead of a JWT. Both are safe to expose with RLS enabled.

Sources

Is your app affected? Find out in a minute.Scan my site