LEAKCADE

How to rotate Supabase keys without downtime

Updated 10 min read

To rotate Supabase keys without downtime, create the new keys first, ship them to every place that holds a key, verify the app works, and only then disable the old ones. Old and new keys work side by side, so the order of steps is what keeps you online.

QUICK CHECKLIST

  • Inventory every place a Supabase key lives
  • Create new publishable and secret keys first
  • Update Vercel env vars and redeploy before revoking
  • Update Edge Function secrets and pg_net callers
  • Smoke test with curl and a real login
  • Deactivate legacy keys last (it's reversible)

Why you can rotate Supabase keys without an outage

Supabase has two key systems that run at the same time. The legacy anon and service_role keys are long-lived JWTs. The newer publishable (sb_publishable_...) and secret (sb_secret_...) keys are short strings, not JWTs. Creating new keys does not revoke the legacy ones. They stay valid until you disable them in a separate step.

That overlap is your safety net. You add the new key, move every caller over, and only then turn the old key off.

Safe vs unsafe rotation orderTwo lanes compare rotation orders. The wrong order revokes the old key first, so apps break. The right order creates new keys, ships them, verifies, then revokes the old key.WRONG ORDER1Revoke the old key2Apps get 401 errors3Scramble to ship new keyRIGHT ORDER1Create new keys2Redeploy with new keys3Verify with curlWRONG ORDER1Revoke the old key2Apps get 401 errors3Scramble to ship new keyRIGHT ORDER1Create new keys2Redeploy with new keys3Verify with curl
Old keys keep working until you disable them, so revoke last.

Supabase says it is deprecating anon and service_role by the end of 2026, so this swap is worth doing even if nothing has leaked.

What breaks if you revoke too early

Rotation goes wrong when the old key is turned off before the last caller has moved. Every caller still holding it gets rejected, and you find out from user complaints.

The usual victims are easy to forget:

On Vercel, environment variable changes only apply to new deployments. An old deployment keeps the old value, and it will fail once the old credential is invalidated. Vercel's own advice is the same order we use here: update Vercel before you invalidate the old credential.

Inventory every place a key lives

You can't rotate what you haven't found. Write a list before you touch anything. For a typical Next.js, Vercel and Supabase app, check these:

If you suspect an old key already sits in your repo, find secrets in git history first. Rotating while the old key is still readable in a commit only helps once the old key is actually dead.

Which keys go where

The rule is simple. Code you ship uses the publishable key. Code you control uses the secret key.

Key to useWhere it lives
Browser, mobile app, CLIPublishable keyPublic env var or bundled in the app
Server route, cron job, workerSecret keyServer-only env var
Edge FunctionSecret keySUPABASE_SECRET_KEYS (injected)
Postgres pg_net or webhookSecret keyVault, sent on the apikey header

Where each new key belongs.

The publishable key is safe to expose because Row Level Security decides what it can reach. If you are not sure your policies hold, read is the Supabase anon key safe to expose and Supabase RLS mistakes before relying on that.

The secret key bypasses RLS completely. It must never reach a browser, which is the exact failure covered in Supabase service role key in browser.

Before and after: the env and client changes

Most of the work in code is swapping which variable you read. The client wiring stays the same.

BEFORELegacy keys (deprecated path)
# .env
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...

// Edge Function
const key = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
AFTERPublishable and secret keys
# .env
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
SUPABASE_SECRET_KEY=sb_secret_xxx   # server only, no NEXT_PUBLIC_

// Edge Function
const key = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default']
Same clients, new variable names. The secret key stays server-only.

On a server route, the secret key is used the same way the old service_role key was:

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

export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SECRET_KEY!
)

Keep that variable server-only. If you prefix it with NEXT_PUBLIC_, your bundler ships it to every visitor. See NEXT_PUBLIC_ environment variables for how that mistake happens.

How to rotate Supabase keys, step by step

Do these in order. Each step is safe to stop after, because the old keys keep working until the last one.

  1. Fix the cause first

    If a key leaked, find out how before you rotate. Rotating while the same leak is still open just leaks the next key. For a breach scenario, follow rotate API keys after a breach as well.

  2. Create the new keys

    Open the Settings > API Keys section of the Dashboard and select the Publishable and secret API keys tab. If you see a Create new API keys button, your project is on legacy keys only. Creating the new keys adds a publishable and a secret key named default next to your legacy ones. For an extra key per backend component, add more secret keys with their own names so you can rotate them independently.

  3. Swap the publishable key in client code

    Replace the anon key with the publishable key in web pages, mobile and desktop apps, and any script you ship. The publishable key has the same low privileges, so your RLS policies behave the same. User sign-in through Supabase Auth is unchanged.

  4. Swap the secret key on your servers

    Replace the service_role key with a secret key in servers, workers and Edge Functions. In Vercel, edit the variable in Environment Variables, pick the right environments (Production, Preview, Development), save, then redeploy from Deployments. For team-level variables, check which projects are linked and redeploy each one.

  5. Update Edge Functions, pg_net and Database Webhooks

    In Edge Functions, read the key from SUPABASE_SECRET_KEYS (a JSON object keyed by name) instead of SUPABASE_SERVICE_ROLE_KEY. In pg_net calls and Database Webhooks, move the secret key from the Authorization: Bearer header to the apikey header. The new keys aren't JWTs, so they're rejected as a Bearer token. Don't hardcode the key in SQL. Store it in Vault and read it at call time.

  6. Verify, then deactivate the legacy keys

    Run the checks in the next section. Only when every component is confirmed on the new keys, deactivate the legacy anon and service_role keys in the same Dashboard section. Deactivation is reversible, so you can re-enable them if you missed a client.

Edge Functions need an extra look

Edge Functions are where this rotation most often breaks. Two things change.

First, the injected variables hold a JSON dictionary, not a plain string:

// before
const secretKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!

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

Second, the platform's verify_jwt check only understands the legacy JWT-based keys. Send the new keys on the apikey header only. If the key also goes in Authorization: Bearer, the platform tries to parse it as a JWT and rejects the request with Invalid JWT. Set verify_jwt = false for functions called with an API key, and authorize the caller in your own code. The @supabase/server SDK's withSupabase wrapper does that for you. Details are in Supabase Edge Functions security.

Secrets you set yourself with supabase secrets set are picked up by your functions immediately, with no redeploy. A secret name can't start with SUPABASE_, because that prefix is reserved.

Test it yourself

Run these against your own project only. You need the project URL and the new publishable key.

First, check the new publishable key works against the REST API. Replace your-table with a table that has a public read policy:

curl -i "https://YOUR-PROJECT.supabase.co/rest/v1/your-table?select=*&limit=1" \
  -H "apikey: sb_publishable_xxx"

A 200 with rows (or an empty list) means the key is accepted. A 401 means the key is wrong or not sent on the apikey header.

Next, check the secret key from your server, never from a browser. Supabase returns 401 if a secret key is used with a browser User-Agent, so test it from a terminal or a server route, and don't paste it into chat or a URL:

curl -i "https://YOUR-PROJECT.supabase.co/rest/v1/your-table?select=*&limit=1" \
  -H "apikey: sb_secret_xxx"

Then walk a real smoke path in the deployed app:

  1. Sign in as a normal user and load a page that reads data.
  2. Write something (create a row) and read it back.
  3. Trigger one server-side path that uses the secret key.
  4. Call one Edge Function and one pg_net or webhook flow, if you use them.

What about the JWT secret and signing keys?

This is where the old advice breaks. Supabase's troubleshooting guide says it is no longer possible to rotate the legacy anon, service_role and JWT secrets. It recommends migrating to asymmetric JWT signing keys and to publishable and secret API keys instead. It also warns that if the legacy JWT secret were regenerated, all current API secrets would be invalidated immediately and connections using them would be severed.

The two migrations are separate. The new publishable and secret keys aren't JWTs, so they no longer depend on your project's JWT secret. But the access tokens Supabase Auth issues to your users are still signed by that shared secret until you move to signing keys.

Signing keys let you rotate without signing users out:

Fix prompt you can paste

Give this to Cursor or Claude Code to build the inventory and make the code changes. Do the dashboard steps and the final revoke yourself.

FIX PROMPT
Help me rotate Supabase keys safely. Do not delete or disable anything.

1. Search the whole repo (including .env*, vercel.json, GitHub Actions
   workflows, supabase/functions, supabase/config.toml, SQL migrations and
   mobile code) for every use of: SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
   NEXT_PUBLIC_SUPABASE_ANON_KEY, any string starting with eyJ or sb_, and
   any pg_net / http_post call that sends an Authorization Bearer key.
   Output a table: file, line, which key, where it runs (browser or server).
2. Replace the anon key with the publishable key (sb_publishable_...) in
   client code, and the service_role key with the secret key (sb_secret_...)
   in server code only. Use env var names, never literal keys.
3. In Edge Functions, read the key with
   JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default'].
   Send API keys on the apikey header, not Authorization Bearer.
4. Make sure no secret key variable has a NEXT_PUBLIC_ prefix.
5. Explain every change and list anything you were unsure about.

After the changes land, redeploy, run the curl checks above, and check your Cursor security settings so the assistant can't read your .env files by default.

Zero-downtime checklist for Next.js, Vercel and Supabase

Print this or paste it into your ticket.

  1. Fix the cause of any leak first.
  2. List every place a key lives: browser, Vercel (all environments), Edge Function secrets, CI, local .env, mobile builds, pg_net and webhooks.
  3. Create the new publishable and secret keys. The old keys keep working.
  4. Swap the publishable key in client code. Swap the secret key on servers.
  5. Update Vercel env vars for Production, Preview and Development, then redeploy every project that uses them.
  6. Update Edge Functions, pg_net calls and Database Webhooks (apikey header).
  7. Run the curl checks and a real login and write test on the deployed app.
  8. Check that old mobile builds and cron jobs are on the new keys, or accept that they will break.
  9. Deactivate the legacy keys. Re-enable them if something was missed.
  10. Delete a compromised secret key only after step 8 is done, because deleting it can't be undone.
  11. Plan the separate move to JWT signing keys.

If you want a second pair of eyes on what your deployed app exposes, Leakcade can scan it.

Further reading

  1. Supabase docs: API keys
  2. Supabase docs: Migrating to publishable and secret API keys
  3. Supabase docs: Rotating anon, service and JWT secrets
  4. Supabase docs: JWT signing keys
  5. Supabase docs: Edge Function environment variables
  6. Vercel docs: Rotating environment variables

FAQ

How do I rotate Supabase keys without downtime?

Create the new publishable and secret keys first, since they run alongside your legacy keys. Update every client and server, redeploy, verify, and only then deactivate the old.

Can I rotate the Supabase service_role key?

Supabase says it is no longer possible to rotate the legacy anon, service_role and JWT secrets, and recommends migrating to secret keys instead. Replace service_role with a new secret key everywhere, then deactivate the legacy keys.

Do I need to redeploy on Vercel after changing a Supabase key?

Yes. Environment variable changes only apply to new deployments, so redeploy every project that uses the variable. Old deployments keep the old value and fail once you disable the old key.

Will rotating Supabase keys sign my users out?

Swapping to publishable and secret keys does not touch user sessions, because Auth still signs user tokens separately. Rotating JWT signing keys also doesn't sign users out if you wait for access tokens to expire before revoking the old key.

What happens if I disable the legacy keys too early?

Any client still using them will be rejected. Supabase notes you can re-activate the legacy keys if you find a client you missed, so this step is reversible. Deleting a secret key can't be undone, so do it last.

Want a second pair of eyes?

We review your whole repo and write a fix prompt for every finding. $19, report within an hour.

Check my repo

Leakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.