For SaaS / AI SaaS Founders · Beginner · Commercial · Solves: Does not know if the admin key is in the browser, Afraid customer data is already exposed, Confused by RLS vs a leaked service_role key, Considering a panic rewrite
Key takeaways
- A service_role or secret key in the Vite client bypasses RLS.
- Search the live bundle for service_role, sb_secret_, and eyJ.
- New secret keys return 401 in Chrome and still work from curl.
- Rotate the leaked key before you keep shipping features.
- Hire for a hardening pass, not a redesign or a panic rewrite.
You do not have a Row Level Security problem yet. You may have a worse one. The admin key that bypasses every policy may already be sitting in the JavaScript your visitors download.
That is the search behind is the Supabase service_role key visible in Lovable apps, and the cousin queries: Lovable API key exposure, VITE_SUPABASE_SERVICE_ROLE_KEY, secret key in the frontend bundle.
The people typing those phrases are usually not security engineers. They are founders who shipped a Lovable, Bolt, v0, or Cursor MVP, connected Supabase because the chat said to, and then saw a Reddit thread or a scanner warning. They want a yes or no. Then they want to know whether they just exposed every customer row.
This is not the same problem as missing RLS
I already wrote where vibe-coded SaaS gets dangerous when RLS is missing. That article is about the public key. The publishable or anon key is supposed to be in the browser. It is safe only if every table has RLS, grants, and policies you have actually tested as a second user.
A leaked service_role or secret key makes those policies irrelevant. The key maps to the Postgres service_role, which has the BYPASSRLS attribute. Policies never apply. Anyone who can present the key can read and write the tables the role can reach.
Supabase's own API keys guide is blunt: use a publishable key anywhere you ship code, and a secret key only on a server, Edge Function, or cron job you control. The building metaphor they use is the right one. The publishable key is taped to the front door and only opens the lobby. The secret key is the master key. It stays in your pocket.
Why Lovable and Vite make this leak so common
Lovable compiles the frontend with Vite. Vite's env docs say any variable prefixed with VITE_ is statically replaced into client source at build time. That is documented behavior, not a bug. VITE_SOME_KEY becomes a string in the bundle. DB_PASSWORD without the prefix does not.
Lovable's Secrets documentation draws the same line. Secrets are for Edge Functions and server-side integrations. VITE_ values belong in .env and are embedded in the client. VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY are supposed to be public. A service_role or secret key is not.
The failure is mechanical. A founder, or the model, needs an admin write from a React hook. Someone adds VITE_SUPABASE_SERVICE_ROLE_KEY, or pastes the long eyJ JWT into createClient on the client. The preview works. The dashboard writes work. The build succeeds. The key lands inside assets/index-*.js as a string literal.
Lovable's security checklist already says the frontend is always public, secrets must not live there, and auth, payments, and validation belong in Edge Functions. The leak happens when the generated app ignores that split because the chat found a faster path.
How to know in five minutes
You do not need a pentest. You need the live URL and Chrome DevTools.
1. Open the published app, not only the Lovable preview editor.
2. Open DevTools. Go to Sources. Search the page (Ctrl-F or Cmd-F across files) for service_role, sb_secret_, VITE_SUPABASE_SERVICE, and eyJ. Legacy service_role keys are long JWTs that start with eyJ. New secret keys look like sb_secret_.
3. Check Network. Filter for supabase.co or your project ref. Look at the apikey header. If it is the JWT or sb_secret_ value, the client is presenting the privileged key.
4. Open the project .env and Secrets panel. A VITE_ name next to a service_role or secret value is already a fail. Lovable will reject a VITE_ name inside Secrets, which is correct. The damage happens when the same value is committed in .env instead.
5. In the Supabase dashboard, open Settings, then API Keys. Note whether the project still relies on legacy anon and service_role keys. Supabase is deprecating those JWT keys by the end of 2026 in favor of publishable (sb_publishable_) and secret (sb_secret_) keys. Both systems can be live at once. Creating the new keys does not revoke the old ones.
Legacy JWT vs the new secret key
| Key in the bundle | What it looks like | From Chrome | From curl or a script |
| Legacy service_role JWT | Long token starting with eyJ | Works. Full BYPASSRLS. | Works. Full BYPASSRLS. |
| New secret key | sb_secret_... | Supabase returns HTTP 401 by matching User-Agent. | Works unless you delete the key. Browser block is not enough. |
| Publishable or anon key | sb_publishable_... or a different eyJ JWT | Expected. RLS and grants decide the rest. | Same. This is the public door key. |
Do not celebrate a 401 in the browser and stop. Supabase's leaked-key procedure says the browser block is not a fix. An attacker who copied sb_secret_ can still call your project from a tool that does not look like a browser. Delete the leaked key after every consumer has moved to a new one.
What this actually costs you
This is not a code-quality nit. It is a customer-data problem.
If the key is in the bundle, a stranger can list emails, reset another user's workspace, export uploaded files, or rewrite subscription state. If you also store Stripe customer IDs or raw PII in Postgres, they can take that too. Your RLS policies will not save you. The role never reads them.
That is why this sits next to why Lovable apps break when real users show up and why live Stripe fails after test mode. Preview hides the blast radius. The first 20 strangers expand it.
If paying customers already exist, treat this as an incident. You do not need me to invent a horror story. The official docs already describe the permission model. The commercial question is whether you keep shipping features on top of an open admin door.
Rotate first. Then change the architecture.
Supabase's leaked-key procedure is the order. Fix the root cause before you rotate, then create a new secret key, replace it everywhere, confirm every consumer moved, and only then delete or deactivate the old one. Deleting a secret key cannot be undone. Deactivating a legacy service_role key is reversible if you missed a worker.
The architecture fix is not a rewrite. The client keeps the publishable or anon key. Privileged work moves to a Supabase Edge Function. Lovable already injects SUPABASE_SERVICE_ROLE_KEY into that runtime. The frontend calls the function with the user JWT. The function checks the user, then does the admin write. The key never returns to the browser.
Then do the RLS job. Enable it on every table. Write policies for anon and authenticated. Run a deny test as a second user. A leaked admin key and an open table are two different ways to lose the same data. You can have both at once.
When a founder should stop prompting and hire
If the key is in the bundle and you already have users, stop adding features. When to hire a developer to productionize a Lovable app still holds: hire when a trust boundary can hurt users or money. An admin key in the client is that boundary.
Do not buy a redesign. Do not buy a Next.js rewrite because the tweet said Vite is unsafe. Vite is doing what it is documented to do. Buy a scoped hardening pass: rotate, remove the key from the client, put admin writes in Edge Functions, prove RLS with a deny test, and make sure the company owns GitHub and the Supabase project. What that pass should cost is a different article. This one is the incident.
My take
Vibe coding is legitimate. Shipping before you validate is also legitimate. Putting the master key in the file the browser downloads is not a vibe. It is a category error the model will happily repeat because the admin write works on the first try.
The useful human job here is not prettier React. It is deciding what must never reach the client, then proving it. Founders who stay in the chat after a leaked service_role key are optimizing the lobby while the master key is taped to the glass.
If you found the key, rotate today. If you cannot tell whether you found it, that is already a hiring signal.
Sources & references
- Supabase API keysSupabase
Publishable key for shipped code. Secret or service_role key bypasses RLS and must stay on a server. Legacy JWT keys deprecated by end of 2026.
- Rotate a leaked or compromised keySupabase
Create a new secret key, replace it everywhere, then delete or deactivate the old one. Browser 401 is not enough.
- Vite env variables and modesVite
VITE_ prefixed variables are bundled into client source at build time and must not hold secrets.
- Lovable SecretsLovable
Secrets stay on Edge Functions. VITE_ values are browser-exposed. SUPABASE_SERVICE_ROLE_KEY is injected for functions.
- Security best practices for Lovable appsLovable
Frontend is always public. Auth, payments, and validation belong in Edge Functions. Review RLS before publish.
- Supabase Edge Function secretsSupabase
SUPABASE_SERVICE_ROLE_KEY is safe in Edge Functions and must never be used in a browser.
- Row Level SecuritySupabase
RLS constrains anon and authenticated. It does not constrain service_role.










