Back to BlogSecurity & Compliance

Lovable App Exposing API Keys or service_role? Fix It in 30 Minutes

Rupak Amin

Founder & Lead Engineer, RAITHub

13 min read

If a secret key is visible in your Lovable app, assume it is compromised. Revoke or rotate it at the provider, move the call into a Supabase Edge Function that reads the key from Edge Function secrets, and redeploy. The Supabase publishable or anon key is meant to be public. A service_role or secret key, an OpenAI key or a Stripe secret key never is.

This guide is for founders who built with Lovable and have just been told, by a scanner, a user or a security researcher, that their app exposes keys. It takes about 30 minutes for a small app with one or two leaked keys. It does not replace a security review, and it does not argue against Lovable; it closes the leak you know about and shows how to find the ones you don't.

Which keys in a Lovable app are actually a problem?

Only secret keys. Some keys are designed to sit in the browser, and "hiding" them achieves nothing. Others grant power that no visitor should have. Sort what you found with this table.

KeyUsually looks likeSafe in the browser?What to do
Supabase publishable keysb_publishable_...Yes, if row-level security is correctLeave it; check RLS on every table
Supabase legacy anon keyA long JWT starting eyJ, role anonYes, if row-level security is correctLeave it for now; plan the move to a publishable key
Supabase secret keysb_secret_...NoReplace and delete it; move the code server-side
Supabase legacy service_role keyA JWT starting eyJ, role service_roleNoEmergency: rotate today; it bypasses all RLS
Stripe publishable keypk_live_...YesLeave it
Stripe secret or restricted keysk_live_..., rk_live_...NoRoll it in the Stripe dashboard; move charges server-side
AI provider keys (OpenAI, Anthropic and others)sk-... and similarNoRevoke it; proxy calls through an Edge Function
Email, SMS and other service keysProvider-specificNoRevoke it; send from the server

Supabase's API keys guide is explicit on the dangerous row: secret keys "bypass every Row Level Security policy you have", because they act as the service_role Postgres role, which has the BYPASSRLS attribute. Anyone holding one can read and change your whole database. The same guide notes that the newer sb_secret_ keys are refused with HTTP 401 when a browser sends them. The legacy service_role JWT has no such guard, which is why it is the key most often found working in front-end code.

To tell a legacy anon key from a service_role key, look at the middle section of the JWT, between the two dots. It is base64-encoded JSON, and it contains "role":"anon" or "role":"service_role". Decode it on your own machine with echo 'MIDDLE_PART' | base64 -d, not on a website: pasting a live secret into an online decoder is another leak.

What is the 30-minute plan?

Find, contain, move, rotate, verify. The order matters, because rotating a key while the code still ships it to the browser just leaks the new one.

MinutesStepDone when
0 to 5Find every secret in the live bundle and the repositoryYou have a written list of keys, where each appears, and what it can do
5 to 10Contain the worst one: revoke a service_role, secret, Stripe secret or AI key now if it can move money or read all dataThe leaked key no longer works, even if a feature is briefly down
10 to 20Move each privileged call into an Edge Function and store the key in Edge Function secretsNo secret appears in the front-end code
20 to 25Create replacement keys, set them as secrets, redeployThe feature works again with a key that has never been in the browser
25 to 30Verify the old key fails, search the new bundle again, check usage logsProof, not assumption, that the leak is closed

On containment: the OWASP Secrets Management Cheat Sheet says keys that were exposed "should undergo immediate revocation". For a key that can read your whole database or charge cards, accept a short outage of that feature rather than leave the key live while you refactor.

How do I find exposed keys in the bundle and the repository?

Search the thing attackers see, then the thing they could clone.

  1. The live site. Open it, then the browser's developer tools. In the Sources panel, search all files for service_role, sb_secret_, sk_live_, sk- and your provider names. In the Network panel, click through the app and inspect request headers: an Authorization: Bearer sk-... header going to an AI provider means the key lives in the browser.
  2. The repository, including history. If the project syncs to GitHub, a key deleted last week is still in old commits. From a clone, run:
git grep -nE "sb_secret_|service_role|sk_live_|rk_live_|sk-proj-|sk-ant-" $(git rev-list --all)
git log --all --oneline -- .env .env.local

The second line shows whether an environment file was ever committed. Also check any variable whose name starts with VITE_. Vite's environment variable guide says such variables "will be exposed in client-side source code after Vite bundling" and "should not contain sensitive information such as API keys". Older Lovable projects are React and Vite apps, so a secret in a VITE_ variable is a secret in the browser.

If the repository is public on GitHub, secret scanning already runs on it: GitHub's secret scanning overview says it scans "your entire Git history on all branches". Check the repository's Security tab for alerts.

How do I move an API call out of the browser?

Put it behind a server endpoint that checks who is calling and holds the key itself. For a Lovable app on Supabase, that is usually an Edge Function. Lovable's Supabase integration docs say secrets "are stored in your Supabase project, where your edge functions can read them; they never appear in your app's code or repository".

A minimal function that keeps an OpenAI key on the server and only serves signed-in users:

// supabase/functions/summarize/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2'

const openAiKey = Deno.env.get('OPENAI_API_KEY')!

Deno.serve(async (req) => {
  // Only signed-in users may spend your AI credit.
  const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '')
  const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
  const { data, error } = await supabase.auth.getUser(token)
  if (error || !data.user) return new Response('Unauthorized', { status: 401 })

  const { text } = await req.json()
  if (typeof text !== 'string' || text.length > 4000) {
    return new Response('Bad request', { status: 400 })
  }

  const upstream = await fetch('https://api.openai.com/v1/responses', {
    method: 'POST',
    headers: { Authorization: `Bearer ${openAiKey}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: 'YOUR_MODEL', input: text }),
  })
  return new Response(await upstream.text(), {
    status: upstream.status,
    headers: { 'Content-Type': 'application/json' },
  })
})

CORS handling is left out for brevity; a browser call needs it. The front end then calls the function instead of the provider, and supabase-js sends the user's session token for you:

const { data, error } = await supabase.functions.invoke('summarize', { body: { text } })

Set the key with supabase secrets set OPENAI_API_KEY=your-new-key, or in the dashboard under Edge Functions secrets. Supabase's Edge Function secrets guide notes that secrets are available immediately, without a redeploy. SUPABASE_URL and the legacy SUPABASE_ANON_KEY are provided to every function by default. According to Lovable's self-hosting guide, apps created from 13 May 2026 use TanStack Start, "which runs server code", so a server function there does the same job.

Two details make the difference between moving a key and protecting it. The input check stops someone sending your function a novel to summarise. A rate limit per user stops a signed-in account from looping it. The rate limiting explainer shows the Postgres-backed limiter this website uses without Redis; its media upload endpoint is limited to 10 requests a minute per IP precisely because each call costs money.

How do I rotate a Supabase service_role or secret key?

For the newer keys, create a replacement in Settings, API Keys, update every server component that used the old one, then delete the old key. Supabase warns that deletion is irreversible, so confirm nothing still depends on it.

For the legacy JWT keys, Supabase lets you deactivate them, and deactivation is reversible. The catch: the legacy anon and service_role keys are managed together, so switch your front end to a publishable key first, or deactivating will break the browser client too. Supabase says it is deprecating the anon and service_role keys by the end of 2026, so this migration is due anyway.

For third-party keys, use the provider's dashboard: roll the Stripe secret key, revoke the AI key and create a new one, regenerate the email provider's key. Then put the new value only in Edge Function secrets.

Do I need to rewrite git history to remove the key?

Usually not, once the key is revoked. GitHub's guide to removing sensitive data says the first step is to revoke or rotate the secret, and that rewriting history is disruptive: commits "may still be accessible" in clones, forks and cached views. A revoked key in old history is harmless. Rewrite history only if the file also held something you cannot rotate, such as personal data.

How do I know whether someone used the leaked key?

Check usage on the provider side for the period the key was exposed. AI providers show usage and spend per key or project; Stripe's dashboard lists API requests and events; Supabase's dashboard has API and database logs. Look for spikes, unfamiliar IP ranges, bulk reads and anything created that your app would not create. If personal data could have been read, you may have notification duties under data protection law. That is general information, not legal advice; confirm with your adviser.

Was CVE-2025-48757 about leaked keys?

Not exactly, and the difference matters. CVE-2025-48757 describes Lovable projects with "insufficient Row Level Security (RLS) policies", where attackers read data using the unprivileged public key. The researcher's statement reports 303 vulnerable endpoints across 170 of 1,645 projects analysed, about 10.3%. Among the data exposed were third-party API keys stored in database tables.

So there are two leaks to close. A secret in the front-end code is the one this post fixes. A public key that works because the tables behind it have no policies is the other, and removing the public key does not fix it: RLS does. Both sit on the list of gaps in making a vibe-coded app production-ready.

How do I stop keys leaking again?

  • A naming rule. Nothing secret in a variable prefixed VITE_ or NEXT_PUBLIC_. If the front end needs it, it is not a secret.
  • Lovable's own scans. Lovable's security documentation says the Quick scan runs when you open the publish dialog and checks database access rules, while the on-demand Deep scan looks for hardcoded secrets, among other things. Lovable also says these tools "do not replace a thorough security review".
  • Push protection. GitHub's push protection blocks recognised secrets before they land in a repository, and is on by default for pushes to public repositories.
  • A test that fails on a leak. Build the app in CI and search the output for secret prefixes; fail the build on a match. It takes a few lines and catches the next generated change.
  • Spending caps. Set a monthly limit and billing alerts at every paid provider, so a leak costs a capped amount.

Why RAITHub for this

  • Proof the leak is closed. A security fix from RAITHub comes with a test proving the unauthorised request is now refused, as the fix one issue page sets out, not a note saying the key was moved.
  • The whole path, not one key. The keys you found are rarely the only ones. The work covers the bundle, the repository history, the Edge Functions, the RLS policies behind the public key, and the rate limits in front of paid calls.
  • Fixed scope. A free 15-minute technical audit, then a written, fixed quote. Your code stays yours, under an NDA.

RAITHub has not published a case study of fixing a Lovable app, so this post makes no such claim; it is engineering guidance based on the vendors' documentation and on the rate limiting and security work on this website.

When you don't need us

  • The only key you found is the publishable or anon key and every table has correct RLS. That is working as designed.
  • It is one AI key and one function. The Edge Function above, a new key and a spending cap will do it.
  • You need a forensic investigation of whether data was taken. That is incident-response work; RAITHub does not offer it.

If the list of keys is long, or a service_role key has been live for months, the codebase probably needs more than one fix; Code Rescue covers that.

Last reviewed: 29 September 2026. Supabase, Lovable, Vite, GitHub, OWASP and CVE sources checked on 29 September 2026.

To have the leak traced, the keys moved and the fix proven with a test, choose "Fix a specific issue" on the contact form. Never paste a live key into the form; describe which provider it belongs to.

Frequently asked questions

Is it safe that my Supabase anon key is visible in my Lovable app?

Yes. The anon key and the newer publishable key are meant for the browser. They are only safe when row-level security with correct policies is enabled on every table the API exposes, so check that instead of hiding the key.

What happens if my service_role key is exposed?

Anyone with it can read, change and delete everything in your database, because it bypasses every RLS policy. Treat it as an emergency: rotate it immediately, move the code that used it to the server, and check your logs.

Where should API keys live in a Lovable app?

In your Supabase project's Edge Function secrets, read on the server with Deno.env.get. Lovable's documentation says secrets stored this way never appear in the app's code or repository.

Should I rotate the key or fix the code first?

Contain first if the key can read all data or move money: revoke it, even if a feature goes down. Then move the call server-side and put the new key only there, so the replacement is never shipped to the browser.

Does deleting the key from my code fix the leak?

No. The old value is still in git history, in deployed bundles and possibly in someone's copy. Only revoking or rotating the key at the provider makes the leaked value useless.

Does Lovable's security scan find exposed API keys?

Lovable's documentation says its on-demand Deep scan checks for hardcoded secrets, and the Quick scan before publishing checks database access rules. Lovable also says the scans do not replace a thorough security review, so verify the results yourself.

lovable exposed api keysservice_role keySupabaseEdge Functionssecret rotationCVE-2025-48757vibe coding security

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.