Supabase publishable key vs anon key: what to switch, and what we tested
Short answer: Supabase’s publishable key (sb_publishable_…) replaces the legacy anon key and is just as public: it’s meant to sit in your front end, and Row Level Security decides what it can reach. In our test it behaved exactly like the anon key. The secret key (sb_secret_…) replaces service_role, bypasses Row Level Security, and must never reach a browser. Supabase is deprecating the anon and service_role keys; both systems work side by side until you turn the old ones off.
Quick answers
Is the Supabase publishable key the same as the anon key?
Same job, new format. Supabase’s docs list anon as the “Legacy version of publishable keys” and say the publishable key “carries the same low privileges as the anon key, so your Row Level Security policies behave the same.” In our test, both keys got exactly the same results on the same tables. Publishable keys are short strings, not JWTs.
Which Supabase key can go in my front end?
Only the publishable key (or the legacy anon key). Supabase: “Anything you ship: browser, mobile app, CLI, script” uses the publishable key. The secret or service_role key must never be in browser code, a public repo, or an environment variable your bundler exposes, such as one starting with NEXT_PUBLIC_ or VITE_.
Do I have to switch right now?
Not today. Supabase says creating the new keys “doesn’t revoke your legacy keys. Both key systems work at the same time,” and that it is deprecating the anon and service_role keys. Switch before Supabase turns them off; we don’t give a date here because Supabase hasn’t published an exact one.
Is it safe that my publishable key is visible?
Yes, by design, but it’s only as safe as your Row Level Security. In our test the publishable key could read exactly the rows a policy allowed and nothing else. In our security test, a table with RLS off let anyone holding the public key read and add rows, and a loose policy let any signed-in user read and edit every profile.
Does the publishable key show my database schema?
No. In our test, asking the API for its schema (GET /rest/v1/) with the publishable key returned 401 “Secret API key required”. The legacy anon key was refused too, with “Only the service_role API key can be used for this endpoint.”
Why do I get “Invalid JWT” after switching?
The new keys aren’t JWTs. Supabase’s migration guide says to send them on the apikey header only; if the key is also on Authorization: Bearer, “the platform tries to parse it as a JWT and rejects the request with Invalid JWT,” and Edge Functions need verify_jwt = false plus their own check. On the database REST API in our test, the publishable key on both headers still worked. We didn’t test Edge Functions.
Does my project still have the legacy anon key?
Check Settings → API Keys. Our test project, created on 2026-10-15, still had an enabled legacy anon key next to its publishable key, according to Supabase’s management API. Older projects may have only legacy keys; Supabase says you’ll see a “Create new API keys” button.
What we tested
On 2026-10-16 at about 4:25 PM ET, on our own Supabase test project (free plan, created 2026-10-15), we made a small table with Row Level Security on: two public rows, two private rows, a policy that lets anyone read the public rows, and no insert, update or delete policies. We sent the same requests to the database REST API with the legacy anon key and with the publishable key, then deleted the table.
| Request | Legacy anon key | Publishable key |
|---|---|---|
| Read a table where a policy allows public rows | 200, the 2 public rows | 200, the same 2 rows |
| Read the private rows | 200, no rows | 200, no rows |
| Insert a row (no insert policy) | 401 “new row violates row-level security policy” | Same |
| Update or delete a row (no policy) | 200, nothing changed | 200, nothing changed |
| Read a table with RLS on and no policy | 200, no rows | 200, no rows |
| Read the API schema (GET /rest/v1/) | 401 “Invalid API key” · “Only the service_role API key can be used for this endpoint.” | 401 “Secret API key required” · “Only secret API keys can be used for this endpoint.” |
| Key on both apikey and Authorization: Bearer | 200 | 200 |
| Sent with a browser Origin and User-Agent | not run | 200 (publishable keys are meant for browsers) |
Result: swapping anon for publishable changed nothing about what the key could reach. Row Level Security gave identical answers. The only difference was the error text on the schema endpoint.
Not tested: a real secret key sent from a browser (Supabase says it returns 401, matched on the User-Agent header; we had no secret key to test with and didn’t create one), the error you get after deactivating the legacy keys, Edge Functions, signed-in users, and whether GitHub secret scanning flags an sb_secret_ key (secret scanning on private repos is a paid add-on, and we won’t push even a dummy secret to a public repo for this). We also didn’t look at the dashboard itself.
The four Supabase keys
| Key | Format | Access | Where it goes |
|---|---|---|---|
| Publishable | sb_publishable_…, short string | Low: Row Level Security applies | Browser, mobile app, CLI, scripts |
| Secret | sb_secret_…, short string | Full: bypasses Row Level Security | Your server, Edge Functions, cron jobs only |
| anon (legacy) | Long JWT starting eyJ | Low, like publishable | Replace with publishable |
| service_role (legacy) | Long JWT starting eyJ | Full, like secret | Replace with secret |
Supabase’s rule of thumb: write your policies for the anon and authenticated roles. “Policies never apply to a secret key, because service_role has the BYPASSRLS attribute.” And a handy tell from its docs: “If a tool, tutorial, or AI assistant tells you to copy a long key that begins with eyJ, it was written for the legacy keys.”
The switch checklist
Based on Supabase’s migration guide, in plain language.
- Create the new keys. Settings → API Keys → Publishable and secret API keys. If you see a “Create new API keys” button, your project only has legacy keys so far. Your old keys keep working.
- Put the publishable key in your front end. Replace the anon key wherever your client is created. Only the publishable key may use your framework’s public prefix (
NEXT_PUBLIC_,VITE_). - Put the secret key on your server only. Never behind a public prefix, never in a repo, never pasted into an AI chat. Supabase lets you make a separate secret key per backend, so one leak means one rotation.
- Send the new keys on the
apikeyheader. Supabase’s guide warns that a new key onAuthorization: Bearercan be rejected with “Invalid JWT” (Edge Functions also needverify_jwt = falseand their own check). - Find every place that still has an old key: shipped mobile apps, CI/CD, webhooks, cron jobs, other services. Supabase: “There’s no automatic usage indicator, so this is a manual check.”
- Deactivate the legacy keys, then test your app. Supabase says this is reversible if you find a client you missed.
- Check your Row Level Security either way. Switching keys doesn’t fix a missing or loose policy. Run our security checklist. Security Advisor says “RLS Disabled in Public”? See the safe fix we tested.
If an AI coding tool built your app
Ask it to replace only the anon key with the publishable key in the client code, and to keep the secret key in server-side environment variables. Don’t paste a secret key into the chat. Then search your code for eyJ and sb_secret_ to make sure no secret ended up in the front end. In our security test, a service-role key planted in front-end code was caught by Aikido, Semgrep and Prodable’s free scan, but GitHub secret scanning didn’t flag it.
If a table your AI tool just created returns 42501 “permission denied for table”, the key isn’t the problem: see our tested fix for Supabase error 42501.
Error messages we saw
| What we sent | What came back |
|---|---|
| No apikey header (key only on Authorization: Bearer, or no key) | 401 “No API key found in request” · “No `apikey` request header or url param was found.” |
| Made-up or wrong-project key | 401 “Invalid API key” · “Double check the provided API key for typos. This API key might also be owned by another Supabase project.” |
| Publishable key asking for the schema | 401 “Secret API key required” |
| Insert blocked by Row Level Security | 401, code 42501, “new row violates row-level security policy for table …” |
What switching does and doesn’t fix
Does: secret keys you can rotate one at a time, one key per service, and (per Supabase) secret keys that refuse to work in a browser.
Doesn’t: fix your policies. In our security test, every scanner missed a policy that let any signed-in user read and edit every profile. The public key is only as safe as your Row Level Security.
Sources
- Supabase docs: API keys (checked 2026-10-16)
- Supabase docs: Migrating to publishable and secret API keys (checked 2026-10-16)
- Our test on 2026-10-16 (Supabase free plan, REST API, anon vs publishable key)
- Our security test of 6 scanners and its test script
Related: add login to your app (users still sign in with their own session; the key switch doesn’t change login) · database for your vibe-coded app · Vibe coding hub
General guidance from one small test project, not a security audit. Never paste a real key into a page, chat or repo; the examples here use placeholders.
Know when Supabase’s keys change again
We re-check our pages every 30 days. Changes that matter go in our free weekly email.
We'll email you a link to confirm. We use Kit to send the email. Unsubscribe anytime. Privacy