What this means
Supabase has two kinds of key. The publishable (or legacy anon) key is designed to be public and relies on RLS. The secret (or legacy service_role) key is for servers only: the service_role Postgres role has the BYPASSRLS attribute, so your policies do not apply to it. Supabase states that a secret key must never appear in a web page, source code or a browser, even on localhost. If yours is in frontend code, treat it as compromised and rotate it.
Why it happens
- The key was stored in a variable starting with NEXT_PUBLIC_ or VITE_. Those prefixes tell the build tool to copy the value into browser code.
- An AI tool hit a row-level security error and swapped the public key for the service_role key to make the error disappear.
- An admin feature, such as deleting a user or listing all accounts, was written in a client component instead of a server route.
- The key was pasted directly into a file while prototyping and then committed.
How to fix it
- 1Rotate first. In the dashboard under Settings, API Keys, create a new secret key.
- 2Replace the old key everywhere your server uses it (hosting environment variables, edge function secrets, CI) and redeploy. Confirm everything works on the new key.
- 3Retire the exposed key. Delete the old secret key, or for a legacy service_role key, deactivate the legacy keys in the same dashboard section. If your frontend still uses the legacy anon key, move it to a publishable key first, so that deactivating legacy keys does not break it.
- 4Remove the key from all frontend code. Store it in a server-only variable with no NEXT_PUBLIC_ or VITE_ prefix.
- 5Move the privileged logic into a server route or edge function that checks who the caller is before acting.
- 6Purge the key from git history with git filter-repo and force-push, following GitHub's guide. Do this last: history cleaning is tidy-up, rotation is the fix.
- 7Review your database and API logs for activity you do not recognise during the exposure window.
import { createClient } from '@supabase/supabase-js';
const admin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!,
{ auth: { persistSession: false } },
);
export async function POST(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) return new Response('Unauthorised', { status: 401 });
const { data, error } = await admin.auth.getUser(token);
if (error || !data.user) return new Response('Unauthorised', { status: 401 });
const { data: profile } = await admin
.from('profiles')
.select('is_admin')
.eq('id', data.user.id)
.single();
if (!profile?.is_admin) return new Response('Forbidden', { status: 403 });
return Response.json({ ok: true });
}How to confirm the fix
- Build your app locally and search the output for the key. No match should be found in the browser bundle.
- Open your live site, open devtools, go to the Sources or Network tab and search all files for sb_secret_ and for the first 20 characters of the old key.
- A legacy key is a JWT. Decode the middle part of any key you find in the bundle. If it shows "role":"service_role" it is the wrong key for the browser. "role":"anon" is expected.
- Call your API with the old key. After rotation it should be rejected.
npm run build
grep -rIl 'sb_secret_' .next/static dist build 2>/dev/null
grep -rIl 'FIRST_20_CHARS_OF_THE_OLD_KEY' .next/static dist build 2>/dev/null
echo 'MIDDLE_PART_OF_A_JWT_KEY' | base64 -dFrequently asked questions
I removed the key from the code. Do I still need to rotate it?
Yes. Anyone who loaded your site while the key was present may have it, and old bundles stay in caches and git history. Only rotation makes the leaked value useless.
Is the anon or publishable key also a problem in the frontend?
No. That key is meant to be public. It is safe as long as row-level security is enabled with correct policies on every exposed table.
Will rotating the key break my app?
Only the places that still use the old key. Follow Supabase's order: create the new key, switch every server component to it, confirm, and only then delete or deactivate the old one.
Does RLS protect me if the service_role key leaks?
No. The service_role role bypasses RLS by design, so policies do not apply to requests made with that key.