Your Supabase service_role key is in your client bundle. Here's what to do in the next 15 minutes.

2026-05-07 · vibecheck team · 8 min read · Incident response

Quick answer If service_role appears anywhere in your client JavaScript, every visitor to your site can read and write every row of every table in your Supabase database, regardless of Row-Level Security. The fix is four steps: revoke the key in the Supabase dashboard, generate a new one, replace it in your server-side code (never client), and audit recent API logs for unfamiliar queries. The longer you wait, the more rows are reachable.

Stop reading this for a second and grep your production bundle for the literal string service_role:

curl -sL https://your-app.com/ | grep -oE 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'

That regex pulls every JWT-shaped token off the page. For each one, paste the middle segment into a base64 decoder (or run echo <segment> | base64 -d). If the decoded payload contains "role":"service_role", the key in your bundle bypasses Row-Level Security entirely. Every visitor with view-source can do everything your database admin can do.

Or skip the grep and use the free vibecheck scanner — it does this exact check, plus a hundred more, in about ten seconds.

What an attacker actually does once they find it

The exact request looks like this:

curl 'https://<ref>.supabase.co/rest/v1/users?select=*' \
  -H 'apikey: eyJ...<your-service_role-key>...' \
  -H 'Authorization: Bearer eyJ...<your-service_role-key>...'

If your users table exists, you just got every row. Same shape works for every other table. Then they POST to /rest/v1/users with a body, and they've written a row. They can DELETE. They can use rpc/ to call your Postgres functions.

This is what the Moltbook breach looked like in January. The startup launched a vibe-coded social network on January 5th. By January 8th, an attacker had pulled 1.5 million API authentication tokens and 35,000 email addresses using exactly this technique, against a Supabase project where the service_role key had been bundled into the React client.

Triage first, understand later. Every minute the leaked key is valid, more data is reachable. Do the four steps below now. Read the rest of this post afterwards.

Step 1 — Revoke the key (90 seconds)

Go to your Supabase Dashboard → Settings → API → Project API Keys. Find the row labeled service_role. Click the small "Reset" button next to it. Confirm the warning dialog.

The old key is now dead. Every server, edge function, cron job, or other consumer that was using it will start returning 401 Unauthorized. This is correct. If you don't know all the places that used it, you have a bigger problem than this rotation — and you'll find them in the next few minutes by watching what breaks.

Supabase rolled out a new key naming scheme in 2026 — you may see publishable and secret instead of anon and service_role. The semantics are identical: secret bypasses RLS, publishable doesn't. Treat them the same way.

Step 2 — Generate the replacement (30 seconds)

Same screen, click "Reveal" next to the new service_role row. Copy the whole JWT — it will start with eyJ and have three dot-separated segments.

Put it in your secret manager. Not in your repo. Not in a comment. Not in a Slack message. The exact place depends on where your server-side code runs:

Step 3 — Replace it in server code, never client (5 minutes)

The service_role key only belongs in code that runs on a server you control. Concretely:

The classic mistake in a Next.js codebase looks like this:

// lib/supabase.ts — DON'T DO THIS
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!  // 🚨 NEXT_PUBLIC = bundled
);

If any client component imports this file, the service_role key gets bundled into client JS. The fix is two clients — one for the browser using the anon key, one for the server using the service_role key:

// lib/supabase-browser.ts — for client components
import { createBrowserClient } from '@supabase/ssr';
export const supabase = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!  // anon, not service_role
);

// lib/supabase-server.ts — for server-only code
import { createClient } from '@supabase/supabase-js';
export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,                    // no NEXT_PUBLIC_
  process.env.SUPABASE_SERVICE_ROLE_KEY!,       // no NEXT_PUBLIC_
  { auth: { persistSession: false, autoRefreshToken: false } }
);

Notice the absence of NEXT_PUBLIC_ on the second one. That single prefix is the difference between "secret" and "every visitor can read it."

Step 4 — Audit what was used while the key was valid (5 minutes)

Supabase Dashboard → Logs → API logs. Filter by the role field. Look for service_role requests from IPs you don't recognize, especially:

If you see anything alarming, you have an obligation: notify affected users, file with regulators if applicable (GDPR, CCPA, your state's breach notification law), and write a short post-mortem. Don't let the urge to make this go away override the obligation to be honest with the people whose data you held.

Preventing it from happening again

Rotation is reactive. The pattern that caused this leak is the actual problem. Two changes prevent it:

1. Make the prefix wrong by default

If you only have one Supabase env var in your project — say, NEXT_PUBLIC_SUPABASE_KEY — you will eventually paste a service_role key into it. Use two clearly-named variables, one for each key, with naming that makes the wrong one impossible:

NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...        # no NEXT_PUBLIC_ prefix, ever

And a runtime guard — first thing in any server-only file:

if (typeof window !== 'undefined' && process.env.SUPABASE_SERVICE_ROLE_KEY) {
  throw new Error('service_role key reached the browser bundle');
}

2. Scan on every deploy

Add a check to your CI or deploy hook that fails the build if the deployed bundle contains a service_role JWT. The vibecheck CLI does this in one command:

vibecheck https://your-deploy-url.com --exit-on critical

That returns non-zero if anything critical is found, which fails the deploy. We're working on a GitHub App that runs this automatically on every push — check the scanner in the meantime.

3. Don't let an AI builder paste secrets for you

Lovable, Bolt.new, v0, and Replit Agent all default to wiring Supabase by inlining keys into the generated code. They don't know which key is which. If you paste a service_role key when prompted for "your Supabase API key," it lands in client JS by default, with no warning. Always paste the anon key in the AI builder. The service_role key only goes in your secrets management.

Frequently asked questions

How do I tell if my Supabase service_role key is exposed?

Open production in your browser, view source, search for eyJ. Decode any JWT you find using base64 on the middle segment. If the payload contains "role":"service_role", that key is in your bundle and it bypasses Row-Level Security. Or use the vibecheck scanner — it does this check automatically.

What can an attacker do with a leaked service_role key?

Read every row of every table, write to every table, execute every Postgres function the role has access to. Row-Level Security is bypassed. The key is roughly equivalent to a database admin password.

Will rotating the service_role key break my app?

Yes — every place that uses the old key will fail until updated. That is the point. If you don't know where else the old key is used, the right move is to rotate now and patch the failures as they surface, rather than leave it valid.

Is the anon key also dangerous in client code?

No — the anon key is meant to be in client code. It's only as dangerous as your Row-Level Security policies allow it to be. The fix for an exposed anon key is not rotation; it's auditing your RLS policies. See our Lovable security checklist for that audit.

What if my key has been valid for weeks already?

Rotate it now. Then audit Supabase API logs for the entire window. If anything in those logs looks like exfiltration of user data, you have a notification obligation under GDPR, CCPA, and most state breach laws. The right time to disclose is when you discover it, not when you've fully scoped the impact.

Inspect your app for leaked keys