Supabase says "Legacy API keys are disabled". How do I fix my app?

Checked on

your app, an Edge Function or a script is still sending an old Supabase key, the long string that starts with eyJ, to a project where those legacy keys have been switched off. Replace the legacy anon key with the publishable key (sb_publishable_...) in browser code, and the legacy service_role key with a secret key (sb_secret_...) in server code, then rebuild and redeploy. Supabase lets you re-activate the legacy keys as a short stopgap, but don't do that if they were turned off because one leaked.

Symptoms

  • Login, signup or any data call fails right after someone "cleaned up" keys in the Supabase dashboard.
  • The response body or logs say Legacy API keys are disabled. (The exact JSON varies; one example is quoted in supabase/cli#3893.)
  • The app worked before a free project was paused and restored, and broke afterwards (see cause 3).
  • An Edge Function that calls Supabase with SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY starts failing while the frontend works.

Why it happens

Supabase now has two key systems. The old ones are the anon and service_role keys, which are long-lived JWTs. The new ones are the publishable key (sb_publishable_..., safe in the browser) and secret keys (sb_secret_..., backend only). Supabase's docs say it "is deprecating the anon and service_role keys by the end of 2026" (Supabase: API keys).

Both systems work side by side until someone deactivates the legacy keys, which is a separate step in the dashboard (Supabase: Migrating to new API keys). Anything still holding an eyJ... key fails from that moment. The three usual ways it happens:

  1. Someone finished the migration but missed a caller. Supabase's migration guide warns: "There's no automatic usage indicator, so this is a manual check." Typical misses: a second app, a cron job, a webhook, a Database Webhook or pg_net call, or an old mobile build.
  2. An Edge Function still reads the legacy variables. Supabase injects SUPABASE_PUBLISHABLE_KEYS and SUPABASE_SECRET_KEYS alongside the legacy SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY, and the legacy ones "carry the legacy keys" (Supabase: API keys).
  3. A paused project was restored. Supabase's announcement timeline says "Projects restored from 1st November 2025 will no longer be restored with the legacy API keys" (supabase discussion #29260). The fix is the same.

AI builders make this more likely: many tutorials and generated code still tell you to paste a long eyJ... key. Supabase's docs say that 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."

Step-by-step fix

1. Find out which caller is failing

Open the browser DevTools Network tab on your live site and retry the failing action. A failing request to https://<project>.supabase.co/... with this message means the problem is in your frontend code. If the frontend is fine, check your Edge Function logs and any server, cron or webhook that calls Supabase.

2. Get the new keys

In the Supabase dashboard, open Settings > API Keys and the Publishable and secret API keys tab. If you only see a Create new API keys button, the project has never had them. Creating them is safe and doesn't touch the legacy keys (migration guide, step 1).

3. Swap the browser key

Replace the anon key with the publishable key wherever the frontend creates its client:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY // was the anon key (eyJ...)
)

The publishable key carries the same low privileges as anon, so your RLS policies behave the same (migration guide, step 2).

If the key comes from an environment variable on Vercel, Netlify or another host, update it there and rebuild. VITE_ and NEXT_PUBLIC_ values are baked in at build time (Vite: Env variables, Next.js: Environment variables).

A community thread reports that an old supabase-js (2.39) failed with the new keys (answeroverflow). If the swap doesn't work, upgrade @supabase/supabase-js to the current release first.

4. Swap server keys and Edge Functions

Replace service_role with a secret key in every backend. In Edge Functions, read the new variable instead of the legacy one (migration guide, step 4):

// before
const secretKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
// after
const secretKey = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default']

Send the new keys on the apikey header, not Authorization: Bearer. They aren't JWTs, so anything that tries to verify one as a JWT fails (Supabase: API keys).

5. Check the easy-to-miss callers

From Supabase's own list: app versions already in users' hands, CI/CD pipelines, third-party integrations and webhooks, cron jobs, workers, pg_net calls and Database Webhooks.

6. Stopgap only: re-activate legacy keys

Deactivation is reversible: "You can re-activate them if you find a client you missed" (migration guide, step 6). Don't re-activate if the keys were turned off because one leaked. That puts the leaked key back in service.

On Lovable Cloud (Lovable's built-in backend) you don't manage the Supabase project directly. Lovable's docs don't describe how it handles this key change (checked 19 Oct 2026), so ask Lovable in the project chat or contact Lovable support rather than editing keys by hand.

Fix prompt for your AI builder

Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with:

Fix prompt
My Supabase calls fail with "Legacy API keys are disabled". Fix only the Supabase key setup, nothing else:
1. Find every place the app or its Edge Functions read a Supabase key (env vars, createClient calls, Deno.env.get).
2. In browser code, use the publishable key (sb_publishable_...) from an env var such as VITE_SUPABASE_PUBLISHABLE_KEY. Never put a secret or service_role key in browser code.
3. In Edge Functions, replace Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') with JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default'], and SUPABASE_ANON_KEY with the matching SUPABASE_PUBLISHABLE_KEYS entry.
4. Remove any hard-coded key that starts with eyJ.
5. List every file you changed and every env var I need to set on my host, then tell me to rebuild.
Don't change auth flows, UI or database policies.

How to confirm it's fixed

  1. Rebuild and redeploy, then open the live site in a private window.
  2. Sign up or log in with a test account and load a page that reads data.
  3. In DevTools Network, check that Supabase requests return 2xx and that no request still carries an eyJ... key in the apikey header.
  4. Trigger each Edge Function, cron and webhook once and check their logs.

How to find out sooner next time

A key change like this breaks things after the button is pressed, and Heedza never submits forms or signs in. So it won't tell you that a login fails. What it can do: Heedza checks on a schedule that your landing, signup, login and checkout pages load and still show their forms. The free scan reads your homepage and a few of its scripts and flags exposed keys, including a Supabase service_role key that has slipped into frontend code during a migration like this one.

Let Heedza check this for you. Heedza’s security checks looks for secret keys left in your page code and private files anyone can open. It checks again every time it checks your pages. It never logs in or fills in forms, so it can’t see errors that only show after that.

Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.

More questions

Is the publishable key safe to show in the browser?

Yes, that's what it's for, as long as Row Level Security is on with the right policies. See Is your Supabase anon key safe in the browser?

Can I just turn the legacy keys back on?

As a short stopgap, yes, unless they were disabled because a key leaked. Supabase's timeline says legacy keys will eventually stop working altogether ("Late 2026, TBC") (discussion #29260), so migrate anyway.

Is "Invalid API key" the same error?

Not necessarily. A third-party fix guide (guardlayer.io) notes it usually means the key doesn't belong to the project at that URL, for example a key copied from another project or a truncated copy. Check that the URL and key come from the same project.

My secret key ended up in frontend code. What now?

Treat it as leaked: rotate it and move the call server-side. See Supabase service_role key exposed?

Where this comes from