How to rotate Supabase keys without downtime
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.
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:
- Mobile or desktop app versions already installed on users' devices.
- Cron jobs, workers and CI/CD scripts.
- Third-party integrations and webhooks.
- Postgres calls through
pg_netand Database Webhooks. - A Vercel preview deployment that still has the old value.
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:
- Browser code: the publishable (or legacy anon) key, usually from
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. It is public by design. - Vercel (or Netlify) env vars: every project and every environment: Production, Preview and Development. Team-level variables can be shared by several projects.
- Edge Function secrets: anything you set yourself with
supabase secrets set, plus the injectedSUPABASE_PUBLISHABLE_KEYSandSUPABASE_SECRET_KEYS. - **Database Webhooks and
pg_netcalls:** these often carry theservice_rolekey in anAuthorizationheader. - CI and deploy scripts: GitHub Actions secrets and similar stores.
- **Local
.envfiles:** yours and your teammates'. - Shipped mobile or desktop builds: the key is baked into the binary.
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 use | Where it lives | |
|---|---|---|
| Browser, mobile app, CLI | Publishable key | Public env var or bundled in the app |
| Server route, cron job, worker | Secret key | Server-only env var |
| Edge Function | Secret key | SUPABASE_SECRET_KEYS (injected) |
| Postgres pg_net or webhook | Secret key | Vault, 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.
# .env
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
// Edge Function
const key = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!# .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']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.
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.
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
defaultnext 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.Swap the publishable key in client code
Replace the
anonkey 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.Swap the secret key on your servers
Replace the
service_rolekey 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.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 ofSUPABASE_SERVICE_ROLE_KEY. Inpg_netcalls and Database Webhooks, move the secret key from theAuthorization: Bearerheader to theapikeyheader. 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.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
anonandservice_rolekeys 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:
- Sign in as a normal user and load a page that reads data.
- Write something (create a row) and read it back.
- Trigger one server-side path that uses the secret key.
- Call one Edge Function and one
pg_netor 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:
- A new key starts as standby, then you click Rotate keys and it becomes the current key.
- The old key moves to Previously used. Non-expired access tokens stay valid, so no one is forced out.
- Revoke the old key only after it is no longer needed. Supabase suggests waiting for your access token expiry plus a margin (for a 1 hour expiry, at least 1 hour and 15 minutes) so active users aren't signed out.
- Every action except permanent deletion is reversible.
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.
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.
- Fix the cause of any leak first.
- List every place a key lives: browser, Vercel (all environments), Edge Function secrets, CI, local
.env, mobile builds,pg_netand webhooks. - Create the new publishable and secret keys. The old keys keep working.
- Swap the publishable key in client code. Swap the secret key on servers.
- Update Vercel env vars for Production, Preview and Development, then redeploy every project that uses them.
- Update Edge Functions,
pg_netcalls and Database Webhooks (apikeyheader). - Run the
curlchecks and a real login and write test on the deployed app. - Check that old mobile builds and cron jobs are on the new keys, or accept that they will break.
- Deactivate the legacy keys. Re-enable them if something was missed.
- Delete a compromised secret key only after step 8 is done, because deleting it can't be undone.
- 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
- Supabase docs: API keys
- Supabase docs: Migrating to publishable and secret API keys
- Supabase docs: Rotating anon, service and JWT secrets
- Supabase docs: JWT signing keys
- Supabase docs: Edge Function environment variables
- 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 repoLeakcade 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