Supabase service role key exposed in frontend code
A service role key in frontend code is a critical issue: it bypasses every row-level security rule you have, so anyone who opens your site can read or change all of your data. The fix is to rotate the key immediately, then move it to a server-only environment variable so it never reaches the browser again.
Applies to Supabase + Next.js, React, ViteSeverity CriticalDetected by static scan, no code execution
Your secret database admin key is sitting in code that ships to the browser. It bypasses every access rule you have. Anyone who opens your site can read or change all of your data.
What the service role key actually is
Every Supabase project comes with two keys. The anon key is public by design: it ships in your frontend, and everything it does is filtered through your row-level security policies. The service role key is the opposite. It's the master key for server-side jobs, and Supabase grants it one defining power: it skips row-level security entirely.
That's not a bug, it's the point. Backend tasks like admin dashboards, cron jobs, and migrations need to touch any row. The key is safe as long as it lives only on a server you control.
Put it in frontend code, and that calculus inverts. Frontend code is not private. Every visitor downloads it. Anyone can open browser dev tools, search the JavaScript bundle for service_role, and copy the key in about a minute. From that moment they hold the same power over your database that your own backend does: read every user's records, edit them, delete them, and your carefully written RLS policies never even get consulted.
How this ends up in AI-built apps
This is one of the most common critical findings in code built with Lovable, Bolt, Cursor, and similar tools, and it usually arrives through one of two doors:
The prompt loop
A query fails because RLS blocks it. The builder asks the AI to fix it. The AI's fastest path to a green result is to swap in the key that ignores RLS. The query works, the chat moves on, and the admin key is now in a component.
The environment variable trap
The key looks safe because it's in an env var. But in Next.js, any variable prefixed NEXT_PUBLIC_ is compiled into the bundle sent to every browser. Same story with VITE_ in Vite apps. This line feels responsible and is fully exposed:
The prefix is doing exactly what it says. It's just saying something you didn't want.
How to fix it
Rotate the key first, before touching the code
In your Supabase dashboard, go to Settings, then API, and rotate the compromised key. If your project uses the newer secret keys (sb_secret_…), you can rotate that key individually. On the legacy JWT-based keys, rotation means resetting the JWT secret, which regenerates the anon key too, so plan to update both.
Order matters:removing the key from your code does nothing while the old key still works. Rotate first.
Move the new key server-side only
Store it as a plain environment variable with no NEXT_PUBLIC_ or VITE_prefix, set in your hosting provider's dashboard (Vercel, Netlify, Railway). Reference it only in server code: API routes, server actions, edge functions.
Give the frontend the anon key and real policies
The browser client should use the anon key, with RLS policies that scope each user to their own rows. If a feature genuinely needs admin power, wrap it in a server endpoint that checks who's asking, and call that endpoint from the frontend.
Redeploy and check your history
Old deployed builds still contain the old key, which is another reason rotation comes first. If the key was ever committed to git, it lives in your history even after the file is deleted. Since you've rotated, the historical copy is dead, but it's worth knowing it's there.
How to verify the fix
service_role and the first characters of the new key. Zero matches.service_role, serviceRoleKey, and SERVICE_ROLE. Matches should exist only in server-side files, if anywhere.If features break after the fix, that's information, not regression. It means parts of your app only worked because access control was being skipped. Those parts need real RLS policies or a server endpoint, and it's much cheaper to find that out now than from an incident report.
Fair questions
Is the anon key also dangerous to expose?
No. The anon key is designed to be public and ships in every Supabase frontend. It only grants what your RLS policies allow. The service role key is the opposite: it bypasses those policies entirely. The danger isn't that a key is visible, it's which key is visible.
My key is in an environment variable, not hardcoded. Am I safe?
Only if that variable never reaches the browser. In Next.js, anything prefixed NEXT_PUBLIC_ is bundled into the JavaScript sent to every visitor, and Vite does the same with VITE_. A service role key behind a public prefix is exactly as exposed as one pasted directly into the code.
Nobody has noticed for months. Do I still need to rotate?
Yes. You can't prove nobody extracted it, and extracting it takes about a minute with dev tools. Once a secret has shipped to browsers, treat it as compromised whether or not you've seen abuse. Rotation is cheap; a quietly copied admin key is not.
Will removing the key from my code fix the problem?
Not by itself. The old key keeps working until you rotate it in the Supabase dashboard, and it may still exist in your git history and in previously deployed builds. The order is: rotate first, then move the new key server-side, then redeploy.
Ascertify is a static code audit for AI-built and agency-delivered apps. It reads your code without running it and reports what's exposed, what's missing, and what to fix first, in plain English, pinned to the exact file and line. This check is one of the launch-blocking issues it looks for; the free preview tells you how many your code has.
Read-only access. Your code is sandboxed, never executed, and deleted after the scan.