Both of your current Supabase keys start with eyJhbGci. The public one and the all-powerful admin one look identical at a glance, which is exactly why the admin key keeps ending up in frontend bundles without anyone noticing.
That changes with the new key format. Supabase is replacing the legacy anon and service_role JWTs with sb_publishable_... and sb_secret_.... Those prefixes are the practical win nobody talks about: sb_secret_ in a deployed JavaScript file, a git diff, or a pull request is instantly, obviously wrong. eyJhbGci is not.
TL;DR
Supabase is retiring the legacy anon and service_role JWT keys. anon becomes sb_publishable_ (one per project, safe in your frontend) and service_role becomes sb_secret_ (several allowed, each individually revocable, server-side only). Both old and new keys work at the same time during migration, and legacy keys stay valid until you disable them yourself. Removal is expected late 2026 but the date is officially TBC. Before you migrate, search your deployed bundle for the admin key, because that's the failure this migration actually exposes.

What Is Actually Changing
Two keys become two key types, with different naming and different revocation behavior.
| Legacy key | New key | Where it belongs | What it can do |
|---|---|---|---|
anon (JWT, eyJhbGci...) | sb_publishable_... | Browser, mobile app, anywhere public | Only what your RLS policies allow. One per project. |
service_role (JWT, eyJhbGci...) | sb_secret_... | Server routes, Edge Functions, CI, cron jobs | Bypasses every RLS policy. Full read and write on every table. |
The permissions model didn't change. A publishable key is still governed by Row Level Security, and a secret key still ignores RLS completely. What changed is the format, the count, and how you revoke them.
Supabase announced this in 2024. The tracking discussion was last updated on 2025-11-01, at which point new projects stopped shipping legacy keys at all. If your project was created before that, you still have both.
Timeline, stated precisely because the details matter: Supabase's tracking discussion (discussions/29260) says removal is expected in late 2026, TBC. The migration guide says legacy keys will be deprecated by the end of 2026. No specific day has been announced. Supabase's own phrasing on what happens then: "You have to migrate to use the new API keys by this point or your app will break."
Why This Is a Security Upgrade, Not Busywork
Two things get genuinely better, and both matter more for AI-built apps than for hand-written ones.
Secret keys are individually revocable. The legacy service_role key is one key for the entire project. If it leaks, you reset it, and every single thing that uses it dies at the same instant: your webhook handler, your cron job, your admin dashboard, the seed script on your laptop, the CI pipeline you set up six months ago and forgot. That fear is why people sit on a known-leaked service_role key for days instead of rotating it. With sb_secret_ keys you create one per consumer, so revoking the leaked one takes down exactly one thing while everything else keeps running.
The prefix is greppable. This one sounds cosmetic. It isn't. A legacy Supabase key in a bundle is a long base64 blob that looks like every other JWT in the file, including the perfectly harmless anon key and any session token. You can't eyeball it. You have to decode the payload and read the role field to know whether you're looking at a public key or a god key.
sb_secret_ is a literal string. It shows up in Ctrl+F in devtools, in grep, in a code review diff, in a secret scanner's ruleset, and in your own memory when you're skimming a file at 1am. The scanners we run at CheckYourVibe already have to decode JWT payloads to tell the two legacy keys apart. With the new format, that step disappears.
Check Whether Your Secret Key Is in Your Frontend
Do this before you migrate, not after. If the legacy service_role key is already in your deployed bundle, migrating without noticing just means you now have a new admin key sitting in the same place.
Search your live bundle.
Open your deployed app, press F12, go to the Sources or Network tab, and search across all files for service_role. Then search for eyJhbGci. Both should turn up either nothing or only your anon key.
If you find a JWT and want to know which one it is, paste the middle segment into the console:
JSON.parse(atob('MIDDLE_SEGMENT_OF_THE_KEY'))
A "role": "anon" result is fine. A "role": "service_role" result means your admin key is public right now, and you should stop reading about migration and go fix that first.
Check for the wrong prefix on the right variable.
This is the single most common way it happens in Lovable, Bolt, and v0 projects. Anything prefixed NEXT_PUBLIC_ or VITE_ gets compiled into the browser bundle by design. If an admin key got assigned to one of those, the bundler did exactly what it was told.
# Any hit here is a problem
grep -rn "NEXT_PUBLIC_SUPABASE_SERVICE_ROLE\|VITE_SUPABASE_SERVICE_ROLE" .
grep -rn "NEXT_PUBLIC_SUPABASE_SECRET\|VITE_SUPABASE_SECRET" .
# After migrating, the new prefix makes this trivial
grep -rn "sb_secret_" src/ app/ components/ pages/
Search your git history, not just your working tree.
Deleting a key from a file doesn't remove it from history. git log -S searches every commit for content containing a string:
# Did this key value ever exist in any commit?
git log -S 'eyJhbGciOiJIUzI1NiI' --oneline --all
# Same check for the new format, going forward
git log -S 'sb_secret_' --oneline --all
# And the usual suspects
git log --all --full-history -- .env .env.local .env.production
If any of these return commits, treat the key as compromised regardless of what your current code looks like.
If you found a live service_role key through any of those checks, our Supabase key exposure guide covers rotation, git history scrubbing, and the server-side pattern that keeps it from happening again. Come back here afterward.
The Migration, in the Order That Won't Break Production
Create the new keys.
Dashboard, then Project Settings > API Keys. Create the publishable key (you get one) and then create secret keys. Don't create just one secret key out of habit. Make one per consumer: sb_secret_ for your server API, another for your Edge Functions, another for CI. That's the entire point of the new format. Name each one after where it lives so a future you knows what breaks if it's revoked.
Update server-side environment variables first.
Server first, because a server running an old key while the client runs a new one is harmless. The reverse is also harmless here (both key sets are live), but server-first keeps the blast radius boring.
Replace SUPABASE_SERVICE_ROLE_KEY with the new sb_secret_ value everywhere it exists. Everywhere is longer than you think: production, staging, preview environments, GitHub Actions secrets, the cron job on Railway, the .env on the laptop you use for seed scripts.
# Server-side only. No NEXT_PUBLIC_, no VITE_, ever.
SUPABASE_URL=https://yourproject.supabase.co
SUPABASE_SECRET_KEY=sb_secret_...
Update client-side environment variables.
Swap the anon key value for the sb_publishable_ value. The variable name can stay the same if renaming it means touching a lot of files, though renaming is cleaner:
# Client-side. Safe to ship, still governed by RLS.
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
Redeploy the frontend.
Verify every environment before you disable anything.
Log in as a real user. Trigger the flows that hit your admin key: the webhook, the scheduled job, the admin screen. Check your Supabase API logs for 401s. Because both key sets are valid at once, a mistake at this stage is invisible until you disable the legacy keys, which is precisely why this step exists.
Disable the legacy keys last.
Once nothing is still calling Supabase with a eyJhbGci key, disable the legacy anon and service_role keys in the dashboard. This is a deliberate action you take, not something Supabase does to you on a schedule (at least not until the removal date lands).
The environments people forget: Vercel and Netlify preview deployments often carry their own copy of environment variables, separate from production. So do staging projects, GitHub Actions secrets, Supabase Edge Function secrets, and any local .env a teammate is running scripts against. Disable the legacy keys while one of those still holds an old value and that thing starts returning 401s with no obvious cause. Grep your infra for eyJhbGci before you flip the switch.
What to Do About the Date
Don't wait for a deadline that hasn't been set. The honest status right now: removal is expected late 2026, Supabase marks it TBC, and the docs say deprecated by end of 2026. That's it. Anyone quoting you a specific day is guessing.
Subscribe to the tracking discussion. It's where the date will be confirmed. In the meantime, the migration itself takes an afternoon on a small app and leaves you with revocable secret keys and a key format you can actually search for, which is worth doing on its own.
Is sb_publishable the same as the anon key?
Functionally, yes. sb_publishable_ is the direct replacement for the legacy anon key. It's meant to sit in your frontend, it identifies your project, and what it can read or write is still controlled entirely by your Row Level Security policies. The difference is the format: the anon key is a JWT starting with eyJhbGci, while the publishable key is a short opaque string starting with sb_publishable_. You get one per project.
When exactly do the old Supabase keys stop working?
There's no fixed date yet. The Supabase tracking discussion says removal is expected in late 2026 and explicitly marks it TBC, and the migration docs say legacy keys will be deprecated by the end of 2026. Until you disable them yourself, your legacy keys keep working alongside the new ones. Watch discussions/29260 for the confirmed date.
What replaces the service_role key?
Secret keys with the sb_secret_ prefix. The meaningful difference is that you can create several and revoke any one of them without touching the others. The legacy service_role key was a single key for the whole project, so rotating it meant every service using it had to be updated in the same moment.
Can I use the old and new Supabase keys at the same time?
Yes, and that's the intended path. Both key sets are valid simultaneously. Move each environment to the new keys one at a time, verify it, and disable the legacy keys only when nothing is still using them.
Do I have to migrate if my app already works fine?
Eventually, yes. Supabase's migration guide puts it plainly: "You have to migrate to use the new API keys by this point or your app will break." New projects already ship without legacy keys. Migrating early also gets you individually revocable secret keys instead of one unrevocable admin key, which is a real improvement.
Scan your deployed app for Supabase admin keys in your JavaScript bundle, tables missing RLS, and open policies. Free, no signup required.