Checklist
Service role key leaked? What to do
Short answer. A service_role JWT or an sb_secret_ key bypasses row level security. If it is in a browser bundle, a public repo, or a chat, treat it as compromised. Take it out of the client first, put a new secret key only on the server, then deactivate the old one. Logs can be incomplete, so also look at the data.
Updated 5 Nov 2026
This is a practical checklist, not a guarantee. It is not a penetration test, a compliance audit, or proof that an app is safe to launch.
The anon key and the publishable key are a different problem. Supabase says those may be in the browser when row level security is on. This page is only the elevated key. The companion page is Is the Supabase anon key safe to expose?.
How to tell if it leaked
Supabase's API key docs describe two elevated keys. The legacy service_role key is a long JWT. The newer secret key starts with sb_secret_. Both bypass row level security. A publishable key (sb_publishable_...) and a JWT whose payload role is anon are not this key.
Check these places:
- The built JavaScript, not only the source. Open the published site, then search the files the browser downloaded for
service_roleandsb_secret_. A JWT is three base64url segments starting witheyJ. Decode the middle segment on your own machine and readrole.service_roleis the leak. Do not paste the key into a website to decode it. - Environment names the framework ships to the browser.
VITE_,NEXT_PUBLIC_, andPUBLIC_are client values. A service role stored under one of those names is public even if the dashboard row looks private. - Git history, a shared
.env, a preview deployment, and the builder chat. Removing the key from the latest file does not remove a commit, a fork, or a transcript that already has it. - A server log or an error page that printed the environment.
Run a free passive check on the public URL. It flags a JWT whose payload role is service_role and shows a four-character preview, not the key. It does not flag every sb_secret_ string, and it cannot see a private repo or a host dashboard. Search those yourself.
Supabase's API key page also says a secret key must not appear in a web page, source, a chat, an email, a URL, or a request header you have not sanitized. Any of those is exposure, even when the app still "works."
Rotate it now
Supabase's rotate-a-leaked-key steps say to fix the cause before you rotate. If the new key lands in the same client file, you have published the replacement. Do the removal and the rotation in one sitting. Do not leave the old key valid until a later cleanup.
Supabase's procedure, from the API keys guide:
1. Create a new secret key under Settings, then API Keys. 2. Replace the compromised key everywhere a server you control still needs it. If the leaked value was a JWT service_role key, Supabase says to replace it with the new secret key, not with another copy of the legacy key. 3. Confirm each server, Edge Function, cron job, and webhook is using the new key. The dashboard shows last-used indicators. A mobile app, a GitHub Action, pg_net, and a database webhook are easy to miss. 4. Retire the old key. Delete a leaked secret key. Deletion cannot be undone, so Supabase says not to delete until step 3 is done. For a legacy anon or service_role key, deactivate the legacy keys in that same Settings page. Deactivation is reversible. Creating a new key beside the old one does not retire the old one. Legacy keys stay valid until you deactivate them.
A newer secret key returns HTTP 401 when it is sent from a browser, matched on the User-Agent. Supabase says that block is not retirement: other tools can still use the key. Deactivate or delete it.
Also move any legitimate caller off the legacy JWT signing secret if you still depend on it. Supabase's signing-keys guide says anon and service_role are themselves JWTs signed by that secret, and that revoking the legacy JWT secret is a separate migration. Disabling the leaked API key is the step that stops this credential. Do not regenerate the project JWT secret as a shortcut unless you have read that guide. Supabase says doing so invalidates current API secrets immediately.
Audit logs and data
Logs are a clue, not a proof that the key was unused.
In the project, open Logs. Supabase's Logs view can show API Gateway, Postgres, Auth, Storage, PostgREST, and Edge Function events for the retention your plan still has. Filter to the window after the key could have leaked. Look for requests you do not recognize. Supabase says an empty result does not establish that there was no activity: the time range, the log type, and whether the event was recorded all change what you see. Retention can also shrink to a day, or to an hour on the Free plan, if log usage is over the allowance. You cannot reconstruct a month from logs that were not kept.
Postgres does not record every statement by default. Supabase's logging docs say extra statement logging applies to future events and cannot recover past activity. Turning on pgAudit now does not show what the key already did. Their pgAudit page notes that API traffic uses the authenticator role, and that API requests are already recorded in the API edge logs.
Platform audit logs are a different product. Supabase says they record organization members' dashboard and platform API actions, and that they are on the Team and Enterprise plans. They are not a list of every row a leaked service_role key read.
Then look at the data the key could change:
- Authentication: users you did not create, and users whose email or password metadata changed.
- Tables the service role can write: rows you did not insert, unexpected deletes, and ownership columns rewritten.
- Storage: objects in buckets the browser should not have been able to fill.
- Edge Functions: a function you did not deploy that uses the secret key.
If you find changes you cannot explain, treat the data as untrusted and restore from a backup you trust. This page cannot tell you which rows are legitimate. A quiet log is not that answer.
Move privileged calls server-side
The client should create the Supabase client with the publishable key, or the legacy anon key. Supabase says those reach only what row level security allows. The secret key is for a server, an Edge Function, or another backend you control.
Supabase's Edge Function docs put SUPABASE_SECRET_KEYS and the legacy SUPABASE_SERVICE_ROLE_KEY in the function environment. Read them there with the server runtime. Do not return the key in a response, and do not pass it through to the browser "just for this admin screen."
A server route that holds the secret key still needs its own check. The key bypasses row level security, so the function has to decide who may call it. Supabase also says a secret key stops bypassing RLS if the request carries a user access token. Their troubleshooting note says an SSR client that copies the user session, or a service client that calls signUp and then keeps that session, no longer acts as service_role. Use a separate server client for admin work, and admin.createUser() when the service client must create a user.
After the move, search the built assets again. The old bundle can stay cached. Confirm the HTML and the JavaScript no longer contain service_role or sb_secret_.
Where Lovable, Bolt, and v0 apps leak it
These are the paths the vendors document, not a count of how often they happen.
Lovable. Secrets are injected into Edge Functions and are not part of the browser bundle. SUPABASE_SERVICE_ROLE_KEY is a reserved name Lovable fills in for the built-in backend or a connected Supabase project. Anything prefixed with VITE_ is embedded in the client at build time and belongs in .env, which Lovable says to keep committed so previews can build. The leak is copying the service role into that .env, into a VITE_ name, into a source file, or into the project chat. VITE_SUPABASE_PUBLISHABLE_KEY is the value that belongs in the client. The longer Lovable pass is Is Lovable secure? What to check before you launch.
Bolt. Bolt's secrets docs say server functions read secrets so private values do not end up in the browser. The database intro lists VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY as separate names. Only the VITE_ names are client variables. Putting the service role in a VITE_ name, or asking Bolt to write a .env that the client then imports, publishes it. Editors and co-owners can view environment variables in the project. That is team access, not a public bundle, and it is still a copy of the key. The launch list is Ship a Bolt app.
v0. v0's security docs say only variables prefixed with NEXT_PUBLIC_ are exposed to the browser, and that v0 warns on that prefix. The full-stack docs say client code must use that prefix to read an env var. A service role under NEXT_PUBLIC_, or a client component that imports the server value, is the leak. The supported place is a server variable without that prefix, read from a Route Handler or Server Action. v0 says it can move a call into those. The route still has to check the caller, because the secret key will not. The launch list is Ship a v0 app.
On all three, a preview URL can be public. If the preview bundle contains the key, the preview is the leak, even when production env vars look correct.
FAQ
Is an anon JWT in the bundle the same incident?
No. Decode the payload. role of anon is the legacy publishable key, which Supabase expects in the client when row level security is on. role of service_role is this incident. A value that starts with sb_publishable_ is the newer public key. A value that starts with sb_secret_ is the newer secret key and should be treated like service_role.
Can I rotate later, after the feature is shipped?
No. Supabase says the old service_role key keeps working until you deactivate it, and that a new secret key beside it does not replace it. Ship the server-side cutover and retire the leaked key before you call the incident finished. Fix the client first so the new key is not in the same bundle.
Will the logs show every row the key read?
No. API gateway logs cover recorded HTTP calls for as long as your plan kept them. Postgres statement logging is off unless you turned it on, and Supabase says enabling it does not recover past activity. Platform audit logs, on Team and Enterprise, record dashboard actions by organization members. Look at the data as well as the logs.
The key was only in a private GitHub repo. Does it still count?
Yes. Supabase's handling rules say a secret key must not be in source control. A private repo is a smaller audience than a browser bundle, and it is still a copy you do not control: CI logs, forks, and people with read access. Retire the key. Deleting the latest file is not the retirement.
Sources
Checked 26 Oct 2026.
Informational only. This page describes checks and vendor documentation. It is not a security audit, a penetration test, or a certification, and it is not a guarantee.
Newsletter
A short weekly note on new tools and security checklists. No spam, unsubscribe anytime.