Short answer: Supabase says it is "deprecating the anon and service_role keys by the end of 2026" (Supabase docs). The replacements are the publishable key (sb_publishable_...), which replaces anon, and the secret key (sb_secret_...), which replaces service_role. Both kinds work side by side, so you can switch one piece at a time, test, and only then turn the old keys off. If your app was built with Lovable, Bolt or another AI tool, check your Edge Functions carefully. That's where most things break.
Is the anon key the same as the publishable key? No. They are different keys that do the same job: Supabase's docs call anon the "legacy version of publishable keys" (Supabase docs). Both are low-privilege and safe in the browser, but the publishable key is a short sb_publishable_... string, not a JWT.
The four keys, in plain words
| Key | Looks like | Safe in the browser? | Replaces |
|---|---|---|---|
| Publishable | sb_publishable_... (short) |
Yes | anon |
| Secret | sb_secret_... (short) |
No, server only | service_role |
anon (legacy) |
Long string starting with eyJ |
Yes | — |
service_role (legacy) |
Long string starting with eyJ |
No, server only | — |
A quick way to tell them apart: the new keys are short strings, while the old ones are long JWTs that start with eyJ. Supabase's docs put it this way: if a tool, tutorial or AI assistant tells you to copy a long key beginning with eyJ, "it was written for the legacy keys" (Supabase docs). Many AI builders and older tutorials still produce the old style, so don't be surprised to find eyJ... keys in a brand-new project.
Why Supabase is changing this
The old keys are long-lived JWTs tied to your project's JWT secret, which made rotating them painful: changing the secret logged everyone out and broke every client at once. The new keys can be created, rotated and deleted one by one. Secret keys also get an extra safety net: if a secret key is used from a browser, Supabase returns HTTP 401 (Supabase migration guide). That doesn't make leaking one harmless, since a script can still use it, but it stops the most common accident: an AI tool pasting the powerful key into frontend code.
What the publishable key does not change
The publishable key has the same low privileges as anon, so "your Row Level Security policies behave the same" (Supabase migration guide). This matters: switching keys does not fix weak database rules. If your tables were readable by anyone with the anon key, they are just as readable with the publishable key. If you haven't checked your RLS policies, do that first. Our guide Is Lovable secure? includes a 20-minute test.
Step-by-step: switching without breaking your app
This follows Supabase's official migration guide, with notes for apps built by AI tools.
1. Find every place the old keys are used
Before you change anything, make a list. Search your code (or your GitHub repo, if your Lovable or Bolt project syncs to one) for eyJ, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, anon and service_role. Also check places that aren't in your code:
- Hosting environment variables (Vercel, Netlify, Lovable, Bolt)
- Supabase Edge Function secrets
- Database webhooks and
pg_netcalls inside Postgres - Automations (Zapier, Make, n8n), cron jobs and CI pipelines
- Mobile apps already installed on users' phones (these can't be updated instantly)
2. Create the new keys
In the Supabase Dashboard go to Settings → API Keys and open the Publishable and secret API keys tab. Supabase creates keys named default next to your existing ones. Nothing stops working at this point.
3. Update the frontend (publishable key)
Replace the anon key with the publishable key wherever your app creates its Supabase client in the browser. With supabase-js it's the same call with a different key:
const supabase = createClient(SUPABASE_URL, 'sb_publishable_...')
Deploy, then log in, sign up and use the main features to confirm everything still loads.
4. Update your server code (secret key)
Replace service_role with a secret key on servers you control: API routes, background jobs, admin scripts. Never put a secret key in a variable that starts with NEXT_PUBLIC_, VITE_ or EXPO_PUBLIC_. Those get bundled into the browser.
For calls from inside Postgres (database webhooks, pg_net), Supabase says to send the key in an apikey header rather than Authorization: Bearer, and to store it in Vault instead of hard-coding it.
5. Update Edge Functions (where AI-built apps usually break)
This is the step that catches people out:
- The built-in
verify_jwtcheck only understands the legacy JWT keys. Functions that use the new keys needverify_jwt = falsein their configuration. - Once that's off, your function code must check who is calling it. Otherwise anyone on the internet can call it. Supabase recommends its
@supabase/serverSDK (withwithSupabase) to handle this, or you can verify the user's session yourself. - New keys are available to functions as JSON in
SUPABASE_PUBLISHABLE_KEYSandSUPABASE_SECRET_KEYS:
const secretKey = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default']
- Send new keys in the
apikeyheader. Putting one inAuthorization: Bearercauses JWT errors.
If an AI tool wrote your functions, ask it (or a developer) to show you exactly where each function checks the user. "It uses the service role" is not an answer.
6. Check Realtime
If your app uses Supabase Realtime without logged-in users, note that "public Realtime connections are limited to 24 hours unless the connection is upgraded with user-level authentication" (Supabase migration guide). Most apps won't notice. Dashboards left open on a wall screen might.
7. Turn off the legacy keys
When everything runs on the new keys, deactivate anon and service_role in Settings → API Keys. Deactivation is reversible, "so you can re-enable them if you find a client you missed." Watch your logs and error monitoring for a few days afterwards.