FixVibe

// docs / baas security / supabase service role exposure

Supabase service role key exposed in JavaScript: what it means and how to find it

The Supabase service role key is the master key to your database. Anyone holding it bypasses Row-Level Security, can read every column of every table, and can write or delete anything they choose. It is designed to live exclusively in server-side code β€” never in the browser. When an AI coding tool ships it to the JavaScript bundle, your database is, in effect, public. This article explains the JWT shape that identifies a leaked key, the three AI-tool patterns that produce the leak, what to do in the first hour after detection, and how to scan for it automatically before users do.

What the service role key is

Supabase issues two distinct keys for every project: the <code>anon</code> key (also called the publishable key in newer projects) and the <code>service_role</code> key. Both are JSON Web Tokens signed by your project's JWT secret. The difference is the <code>role</code> claim baked into the JWT payload β€” <code>anon</code> for the public key, <code>service_role</code> for the master key. PostgREST, Supabase Storage, and Supabase Auth all switch into bypass-everything mode when they see the <code>service_role</code> claim.

Decode any Supabase key at <code>jwt.io</code> and look at the payload. The shape of a service-role JWT is unmistakable:

Decoded payload of a service-role JWT (shown as a syntax-highlighted block below).

json
{
  "iss": "supabase",
  "ref": "[project-ref]",
  "role": "service_role",
  "iat": 1700000000,
  "exp": 2000000000
}

Newer Supabase projects issue secret-style keys with the prefix <code>sb_secret_</code> instead of a JWT. The behaviour is identical β€” anything carrying <code>sb_secret_</code> in a public bundle is equally catastrophic.

How AI coding tools leak the service role key

We've seen the same three patterns across thousands of vibe-coded apps. Each one starts with a developer asking an AI tool for help and ending with the service key inlined into a bundle.

Pattern 1: Single .env file with NEXT_PUBLIC_ prefix

The developer asks the AI tool to "set up Supabase" and accepts a single <code>.env</code> with both keys. The AI tool β€” trained on a corpus where most environment variables are exposed via <code>NEXT_PUBLIC_*</code> β€” prefixes both with <code>NEXT_PUBLIC_</code>. Next.js inlines anything matching that prefix into the client bundle at build time. Ship to Vercel, and the service key is in <code>main.[hash].js</code>.

Pattern 2: Wrong key in createClient call

The developer pastes both keys into a <code>config.ts</code> file the AI generated, and the AI populates the browser-side <code>createClient()</code> call with <code>process.env.SUPABASE_SERVICE_ROLE_KEY</code> by mistake. The build pulls the variable in, and the JWT lands in the bundle.

Pattern 3: Service-role key hardcoded in seed scripts

The developer asks the AI tool to write a script that seeds the database. The AI hardcodes the service-role key directly into the file (rather than reading from environment), commits the file to the repository, and the public GitHub repo or the deployed app's <code>/scripts/seed.js</code> route is now serving the key.

How the FixVibe bundle scan detects the leak

FixVibe's bundle-secrets check downloads every JavaScript file referenced by the deployed app β€” entry chunks, lazy-loaded chunks, web workers, service workers β€” and runs them through a detector that decodes anything matching JWT shape (<code>eyJ[base64-header].eyJ[base64-payload].[signature]</code>). If the decoded payload contains <code>"role": "service_role"</code>, the scan reports it as a critical finding with the file path and the exact line where the key appears. The same check also matches the newer <code>sb_secret_*</code> pattern by prefix.

The scan never authenticates with the discovered key. It identifies the shape and reports the leak β€” using the key to prove exploitability would be unauthorised access to your database. The proof is in the JWT payload itself.

Detected β€” what to do in the first hour

A leaked service role key is a runtime emergency. Assume the key has been scraped β€” attackers monitor public bundles in real time. Treat the database as compromised until you have rotated the key and audited recent activity.

  1. <strong>Rotate the key immediately.</strong> In the Supabase Dashboard, go to Project Settings β†’ API β†’ Service role key β†’ Reset. The old key is invalidated within seconds. Any service-side code using the key must be updated and redeployed before the rotation lands.
  2. <strong>Audit recent database activity.</strong> Open Database β†’ Logs in the dashboard. Filter on the last 7 days. Look for unusual <code>SELECT *</code> queries against tables with PII, large <code>UPDATE</code> or <code>DELETE</code> statements, and requests from IPs outside your known infrastructure. Supabase logs the <code>x-real-ip</code> header on every request.
  3. <strong>Check storage objects.</strong> Visit Storage β†’ Logs and review recent file downloads. A leaked service-role key gives bypass-everything access to private buckets too.
  4. <strong>Remove the key from source control.</strong> Even after rotation, leaving the JWT in your git history means it's discoverable in the public repo. Use <code>git filter-repo</code> or BFG Repo-Cleaner to scrub it from history, then force-push (warn collaborators first).
  5. <strong>Re-scan after fix.</strong> Run a fresh FixVibe scan against the redeployed app. The bundle-secrets finding should clear. Confirm no <code>service_role</code> JWT and no <code>sb_secret_*</code> string remains in any chunk.

Preventing the leak in the first place

The structural fix is naming discipline plus tool-level guardrails:

  • <strong>Never prefix the service key with <code>NEXT_PUBLIC_*</code>, <code>VITE_*</code>, or any other bundle-inlining prefix.</strong> The naming convention is the boundary β€” every framework respects it.
  • <strong>Keep the service key out of <code>.env</code> entirely on the developer machine.</strong> Read it from a secret manager (Doppler, Infisical, Vercel encrypted env vars) on deploy, never commit it locally.
  • <strong>Mark every Supabase client construction with explicit context.</strong> Files named <code>supabase/browser.ts</code> use the anon key; files named <code>supabase/server.ts</code> use the service-role key with <code>import 'server-only'</code> at the top. The <code>server-only</code> import causes a build error if a client component tries to consume the module.
  • <strong>Add a pre-commit hook that greps for JWT-shaped strings.</strong> <code>git diff --staged | grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'</code> catches both anon and service tokens before they leave your machine.
  • <strong>Add a CI gate that scans the build output.</strong> After <code>next build</code>, grep the <code>.next/static/chunks/</code> output for the <code>service_role</code> string. Fail the build if anything matches.
bash
# Pre-commit hook: refuse any staged JWT-shaped string.
git diff --staged \
  | grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' \
  && echo "JWT detected in staged changes β€” refusing commit" \
  && exit 1

# CI gate: fail the build if "service_role" shipped to the static bundle.
grep -RE 'service_role|sb_secret_' .next/static/chunks/ \
  && echo "Service-role credential leaked into bundle" \
  && exit 1

Frequently asked questions

How fast do attackers actually find leaked Supabase service-role keys?

Public-bundle scanners trawl new deployments within minutes. Researchers have documented working exploits against new Supabase projects in under an hour from first deploy. Treat any service-role exposure as a 60-minute window, not a 60-day one.

Is rotating the key enough, or do I have to assume data exfiltration?

Rotation invalidates the leaked key but does not undo data already pulled. If your tables contain PII, payment data, or any regulated data, you may have a notification obligation under GDPR (72 hours), CCPA, or HIPAA. Audit the logs and consult legal counsel if the audit shows suspicious access.

Can RLS protect me if the service-role key leaks?

No. Row-Level Security is bypassed entirely by the <code>service_role</code> claim. That is by design β€” the key exists precisely to let backend code skip RLS for admin operations. The mitigation is to make sure the key never reaches a context where an attacker can read it.

Does this apply to the new Supabase publishable / secret key model (<code>sb_publishable_</code> / <code>sb_secret_</code>)?

Yes β€” identical risk class. The <code>sb_secret_*</code> key is the new secret-key format that replaces the service-role JWT for newer projects. Anything carrying <code>sb_secret_*</code> in a bundle is just as catastrophic as a leaked service-role JWT. FixVibe's bundle-secrets detector matches both shapes.

What about the anon / publishable key β€” is that safe in the bundle?

Yes, by design. The anon key is intended to live in the browser and is what every Supabase web client uses. Its safety depends entirely on RLS being correctly configured on every public table. See the <rlsArticle>Supabase RLS scanner</rlsArticle> article for what to check.

Next steps

Run a FixVibe scan against your production URL β€” the bundle-secrets check is free, no signup, and reports <code>service_role</code> exposure in under a minute. Pair this with the <rlsArticle>Supabase RLS scanner</rlsArticle> article to verify the RLS layer is doing its job, and the <storageArticle>Supabase storage bucket security checklist</storageArticle> to lock down file access. For background on why AI tools generate this leak class so reliably, read <aiGaps>Why AI coding tools leave security gaps</aiGaps>.

// scan your baas surface

Find the open table before someone else does.

Drop in a production URL. FixVibe enumerates the BaaS providers your app talks to, fingerprints their public endpoints, and reports what an unauthenticated client can read or write. Free, no install, no card.

  • Free tier β€” 3 scans / month, no signup card.
  • Passive BaaS fingerprinting β€” no domain verification needed.
  • Supabase, Firebase, Clerk, Auth0, Appwrite, and more.
  • AI fix prompts on every finding β€” paste back into Cursor / Claude Code.
Run a free BaaS scan β†’

no signup required

Supabase service role key exposed in JavaScript: what it means and how to find it β€” Docs Β· FixVibe