Supabase anon key vs service_role key: which goes where
The Supabase anon key, now called the publishable key, is meant to ship in your browser code, and it can only reach what Row Level Security allows. The service_role key, now called the secret key, bypasses Row Level Security entirely and must never appear in a browser, a bundle or a repo. If your anon key is public and your data still leaks, look at your RLS policies and table grants, because the anon key was always public.
On this page9 sections
- What the anon and service_role keys are called now
- Is the Supabase anon key safe to expose?
- What the service_role key can do
- Where to find your Supabase anon key and service role key
- How to check which key is in your JavaScript bundle
- What can someone do with your anon key?
- What to do if your service_role key leaked
- Which key goes where: a short checklist
- Sources
What the anon and service_role keys are called now
Supabase renamed its keys: the anon key’s replacement is the publishable key (sb_publishable_...) and the service_role key’s replacement is the secret key (sb_secret_...). The old anon and service_role keys still exist and Supabase’s docs now label them “legacy”.
The legacy keys are long-lived JWTs, so they start with eyJ. The new keys are short strings. Supabase’s API keys guide puts it plainly: if a tool, tutorial or AI assistant tells you to copy a long key beginning with eyJ, it was written for the legacy keys.
Both systems work at the same time. Creating a publishable or secret key doesn’t revoke the legacy pair, which stays valid until you disable it in Settings > API Keys. That matters if you migrated your app but never turned the old keys off: the old service_role JWT still works for anyone who has a copy.
| Where the code runs | New key | Legacy key | Postgres role |
|---|---|---|---|
| Browser, mobile app, CLI you ship | Publishable sb_publishable_... |
anon (JWT) |
anon, or authenticated once a user signs in |
| Your server, Edge Function, cron job | Secret sb_secret_... |
service_role (JWT) |
service_role (bypasses RLS) |
Is the Supabase anon key safe to expose?
Yes. Supabase designed the publishable key, and the legacy anon key before it, to be readable by anyone. Its docs list web pages, mobile apps, CLIs and source code as places where the key is “safe to expose”, because anyone can pull it out of a bundle anyway.
What the key unlocks is a separate question. Every request made with the publishable key runs as the Postgres anon role, or as authenticated if the user has signed in through Supabase Auth. Your RLS policies and table grants for those two roles decide what comes back.
Wiz’s write-up of the Moltbook exposure (February 2026) shows how this plays out. Wiz found a Supabase URL and an sb_publishable_ key hardcoded in a production JavaScript chunk, and noted that finding the key “does not automatically indicate a security failure.” The failure was behind it: Moltbook had no RLS policies doing the job, so the key gave read and write access to every table. Wiz reported 1.5 million API authentication tokens and 35,000 email addresses exposed.
What the service_role key can do
The service_role key, and the secret key that replaces it, has full access to your project’s data. Both authenticate as the service_role Postgres role, which has the BYPASSRLS attribute, so no policy you write applies to it.
Supabase’s docs say policies “never apply to a secret key.” Anyone holding it can read, change or delete any row the service_role grants allow.
The new secret keys add one guard. Supabase checks the User-Agent header and returns HTTP 401 if a secret key is used from a browser. That stops your own frontend from quietly working with the wrong key. It doesn’t stop an attacker, and Supabase says so directly: “an attacker can still use the key from other tools.” A copied secret key works fine from curl or a script.
The legacy service_role JWT has no browser block at all.
Where to find your Supabase anon key and service role key
Both keys are in the Supabase Dashboard under Settings > API Keys, the one place where every key lives, legacy or new. Supabase notes there is no separate Settings > API page, so a tutorial that sends you there was written for an older Dashboard.
For quick setup, the project’s Connect dialog shows your project URL and publishable key formatted for the framework you pick. If your project has no publishable or secret key yet, the API Keys page shows a Create new API keys button. Creating them adds a publishable and secret key named default next to your existing legacy keys.
For local development, supabase start prints a publishable key and a secret key for the local stack.
Keep the secret key’s value out of your code. Read it by name from an environment variable on the server, and never give that variable a prefix your framework bundles for the browser. In Next.js that prefix is NEXT_PUBLIC_; the Stripe version of this mistake works the same way.
How to check which key is in your JavaScript bundle
Search your built client files for the secret key prefix and for any JWT, then decode each JWT to see which role it carries. Anything a browser downloads is public, so the build output is the place to look.
Here is an example script for a Next.js app. Run it on your own project after next build. The client chunks live under .next/static, which is what the site serves at /_next/static/.
# Example: scan your own Next.js client build for Supabase keys.
# Run from the project root after `next build`.
# 1. New-style secret keys never belong in client files.
grep -rEo 'sb_secret_[A-Za-z0-9_-]+' .next/static && echo "FOUND a secret key in the client bundle"
# 2. Legacy keys are JWTs. Print each one's role claim.
grep -rEoh 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' .next/static | sort -u |
while read -r jwt; do
payload=$(echo "$jwt" | cut -d. -f2 | tr '_-' '/+')
# pad base64 to a multiple of 4
while [ $(( ${#payload} % 4 )) -ne 0 ]; do payload="$payload="; done
echo "$payload" | base64 -d 2>/dev/null | grep -o '"role":"[a-z_]*"'
doneA "role":"anon" result is the legacy anon key, which is expected. A "role":"service_role" result means the full-access key is in every visitor’s browser. Supabase’s JWT docs describe the role claim as “the Postgres role to use when applying Row Level Security policies,” which is why it tells you what the key can do.
You can do the same check by hand in the browser: open DevTools, load your site, and search the Sources or Network panel for sb_secret_ and eyJ.
The same script finds nothing when a key never leaves your server, so also check .env files in git history and any NEXT_PUBLIC_ variable whose name contains SERVICE, SECRET or ROLE.
What can someone do with your anon key?
Someone with your anon key can do whatever your grants and RLS policies allow the anon role to do, and once they sign up for an account, whatever authenticated users can do. That’s why the anon key’s safety depends on your database setup.
Two Supabase defaults make this wider than most people expect. On existing projects, a new table in public starts with every privilege granted to anon, authenticated and service_role. Adding policies doesn’t take those grants back. The docs warn that a table protected only by policies “still hands anon an insert path if you never revoke the grant.”
UpGuard’s September 2026 study adds a second point. It reports that Supabase now enables RLS by default for tables created in the Table Editor, while tables created programmatically through the API, which is how coding agents work, do not get it by default. Across about 300,000 domains with signs of Supabase use, UpGuard identified 16,326 databases exposing readable tables.
The way to know what your anon key can reach is to test it as two different users. Our two-account RLS test walks through that with SQL you can run locally.
What to do if your service_role key leaked
Fix whatever leaked the key first, then replace it with a new secret key and retire the old one. Supabase’s rotation guide gives that order because a new key can leak the same way as the old one.
The steps, condensed from Supabase’s guide:
- Find and close the leak: remove the key from client code, the repo or the log where it showed up.
- Create a new secret key in Settings > API Keys and put it everywhere the old key was used on the server.
- If the leaked key was the legacy
service_roleJWT, also switch your browser and mobile clients from the legacyanonkey to a publishable key. Deactivating the legacy keys turns off both of them. - Confirm every component uses the new keys.
- Retire the compromised key: delete it if it was a secret key, or deactivate the legacy keys if it was
service_role. - For a leaked legacy
service_rolekey, also revoke the legacy JWT secret. The legacy keys are JWTs signed by that secret, so anything still trusting it can accept the leaked key.
If someone is actively using the leaked key, Supabase says to retire it right away, even before every component has moved.
Removing the key from your code doesn’t revoke it. Anyone who loaded your site while it was in the bundle may have a copy.
Which key goes where: a short checklist
- Browser, mobile app, anything you ship: publishable key (or legacy anon key), and RLS enabled on every table it can reach.
- Server routes, Edge Functions, cron jobs, admin tools: secret key, read from an environment variable, never prefixed for the browser.
- One secret key per backend component, so one leak means one rotation.
- Legacy keys disabled once nothing uses them.
- Table grants for
anonandauthenticatedtrimmed to what the app needs.
Shipmate’s free check is passive and needs your written permission. It looks at what your live site already sends to every visitor, including whether a secret key is in the JavaScript. The Launch Lockdown goes further: it tests whether your policies hold and delivers the fixes as a tested pull request.
Sources
- Supabase Docs, “API keys”, https://supabase.com/docs/guides/getting-started/api-keys (accessed 2026-11-07)
- Supabase Docs, “Migrating to publishable and secret API keys”, https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys (accessed 2026-11-07)
- Supabase Docs, “Rotate a leaked or compromised API key”, https://supabase.com/docs/guides/security/rotate-leaked-api-keys (accessed 2026-11-07)
- Supabase Docs, “Row Level Security”, https://supabase.com/docs/guides/database/postgres/row-level-security (accessed 2026-11-07)
- Supabase Docs, “JSON Web Token (JWT)”, https://supabase.com/docs/guides/auth/jwts (accessed 2026-11-07)
- Wiz, “Hacking Moltbook: The AI Social Network Any Human Can Control” (2026-02-02), https://www.wiz.io/blog/exposed-moltbook-database-reveals-millions-of-api-keys (accessed 2026-11-07)
- UpGuard, “Everything Everywhere: Systemic Data Exposure in Supabase Apps” (2026-09-24), https://www.upguard.com/blog/everything-everywhere-systemic-data-exposure-in-supabase-apps (accessed 2026-11-07)