Skip to content
Burnproof

Checklists

Before you publish your Lovable app: a bill-shock and security checklist

NextPlay Solutions · October 18, 2026

One key in the browser is enough. A stranger does not need an account on your Lovable app. If they can copy the key, the invoice goes to the project that issued it, which is you or your client. On September 23, 2025, a student posted in r/googlecloud that a Gemini key left on GitHub became a $55,444.78 bill. That write-up is what the poster said happened. A bill that size can wreck a student, a freelancer, or the client who owns the account. The charge works the same way when a coding agent puts the key in the client so the demo runs.

Where the key ends up

Lovable generates frontend code that runs in the browser. Lovable's own security docs say an API key cannot be stored safely in that code. The usual leak is a VITE_ variable or a key written straight into a React component so the demo works.

Anything prefixed with VITE_ is a build-time value. Lovable embeds it in the client bundle and keeps it in the project .env file, not in Secrets. VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY are the public examples. A private key in a VITE_ name is public. Secrets.

Private values such as OPENAI_API_KEY or STRIPE_SECRET_KEY belong in Lovable Secrets. Lovable says those values are encrypted, injected into Edge Functions at runtime, and never included in frontend code. After you save one, Lovable does not show the value again. You replace it or delete it. Lovable also says not to add .env to .gitignore, because the preview and the published build need the committed VITE_ values. That file is in the repo, so it may contain only values that are safe to publish.

If you paste a key into the project chat, Lovable says it tries to recognize the tool and save the key as a secret or a connector instead of hardcoding it. That is a guide, not a proof the component is clean. Search the published site anyway. SUPABASE_SERVICE_ROLE_KEY is already injected into the Edge Function environment. It does not belong in a React file. Security overview.

Search the live bundle by hand

Open the published URL in Chrome. Open DevTools, then Sources. Press Ctrl+Shift+F on Windows or Linux, or Cmd+Shift+F on a Mac, and search every loaded file. Run these searches one at a time:

  • sk- and sk-ant- for OpenAI and Anthropic
  • AIza for a Google or Gemini key
  • sk_live_ and rk_live_ for a Stripe secret or restricted key
  • service_role for a legacy Supabase key
  • sb_secret_ for a current Supabase secret key

A hit in the HTML, a JavaScript file, or a source map means that value shipped to the browser. The Supabase publishable key (sb_publishable_) and the legacy anon key are supposed to be there. A provider key, a service_role JWT, or an sb_secret_ key is not. Then rotate every key you found. Deleting it from the project does not un-leak a bundle people can already download.

Move the call into an Edge Function

Describe the behavior in the project chat and tell Lovable to call the provider from an Edge Function. Lovable's Supabase docs say it writes the function, deploys it, and updates the app to call it. The function reads the key with Deno.env.get. The browser calls your function. It never sees the key. Connect to Supabase.

If the project uses an external Supabase connection instead of Lovable Cloud, secrets are stored in that Supabase project. The backend view has a Manage secrets button that opens the Supabase dashboard. Lovable says it does not read stored secret values back.

An Edge Function that anyone can call, with no limit, is still your meter. Ask for a check that the caller is allowed to use the feature, and a limit on how often it runs.

Supabase keys and row level security

Supabase is deprecating the long-lived anon and service_role JWTs by the end of 2026. The replacements are a publishable key (sb_publishable_) and a secret key (sb_secret_). Both systems work at the same time until you disable the legacy keys. API keys.

The publishable key, and the legacy anon key, are supposed to be in the browser. They only reach what Row Level Security allows. A secret key and a service_role key bypass those policies. Supabase says a new secret key does not work in a browser: the API returns 401 based on the User-Agent. That block does not save you. Supabase also says an attacker can still use the key from other tools, and the legacy service_role JWT does not have that browser check. If either value is in the bundle, rotate it after the client code is fixed. Row Level Security.

In Lovable, review policies under More, then Cloud, then Database, then RLS policies. The Quick scan runs when you open the publish dialog. Lovable says it checks database access rules (tables without per-record access control, and rules that let everyone through), known vulnerable npm dependencies, and an MCP server exposed without authentication. It does not, in that documented list, search the published JavaScript for sk-.

The Deep scan is the one that includes leaked secrets in source code or the repository. It does not run on every edit. Run it from the Security view before you publish. Lovable says the scans cannot guarantee the app is secure. After you remove a key, publish again, then search the live bundle. A key that already shipped is still in the copy people downloaded.

Check /.env and /.git

In the address bar, open https://your-domain/.env and https://your-domain/.git/config, using the host you are about to share. A real env file is lines of NAME=value. A git config contains [core] and a remote section. A 404, a login wall, or the app's HTML page is what you want. If either file comes back, treat every value in it as public and rotate those credentials.

Set a spend cap that stops the meter

The cap lives on the provider account, not in the coding tool. Set it before you share the link. In the Reddit thread, nothing says a budget alert stopped the usage. A notification you never see is not a lock on the key. These are the controls each provider documents. Enforcement is not always instant, so a limit can still be passed by a little.

  • OpenAI: open Organization limits, edit the monthly spend limit, and turn on Enforce a hard limit. A spend alert only sends a notification. Traffic continues. OpenAI says a hard limit makes later requests fail, and that recorded spend can slightly exceed the number while the limit propagates. Spend limits.
  • Anthropic: in the Claude Console, open Settings, then Billing, then Spend limits, and choose Adjust limit or Set limit. The limit you set cannot exceed the tier cap. Rate limits.
  • Gemini API: AI Studio has a project monthly spend cap under Spend, and a separate cap for the billing-account tier. Google marks the project cap experimental. Billing data can lag by about ten minutes, long-running tasks can go past the cap, and invoiced accounts are excluded. Gemini API billing.
  • Google Cloud: an alerts-only budget does not set a hard cap. Google says so. A spend-cap budget can pause one eligible service in one project, and Google says that enforcement is not instant and in-flight requests can still be billed. Budget alerts · Spend cap budgets.
  • Supabase, on the Pro plan: the Spend Cap disallows further use of a covered item after its quota, until the next billing cycle. It does not cover Compute, branching, read replicas, custom domains, extra disk performance, IPv4, log drains, phone MFA, or point-in-time recovery. It is not a dollar amount you type in. Cost control.

Stripe is different. Stripe documents billing thresholds and usage alerts for what you charge your own customers. Those controls do not stop a leaked sk_live_ key from creating charges on your Stripe account. If that key is in the bundle, roll it in the Stripe dashboard. The publishable key, pk_, is the one that belongs in the browser.

Paste this into the agent

Paste the prompt below into the project chat. Let the agent move the code, then rotate any key that was already public. The prompt tells the agent not to print the key.

You are fixing a Lovable app before it is published. A provider key in the browser can be copied by anyone who loads the site. OpenAI, Anthropic, Gemini, and Stripe bill the account that issued the key.

Do this:
1. Search the project for sk-, sk-ant-, AIza, sk_live_, rk_live_, service_role, and sb_secret_. Remove every provider secret and every Supabase secret key from React components, from any VITE_ variable, and from the committed .env file.
2. Lovable embeds every VITE_ value in the client bundle at build time. The committed .env may contain only public values, such as VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY or the legacy anon key. Do not add .env to .gitignore. Do not put a private key in that file.
3. Store private keys in Lovable Secrets (More, then Cloud, then Secrets), or in the connected Supabase project's secrets if this project uses an external Supabase connection. Do not paste the key into the chat.
4. Move each provider call into a Supabase Edge Function. Read the key with Deno.env.get. The frontend calls the function and never receives the key.
5. Leave the publishable key or legacy anon key in the browser. Do not put SUPABASE_SERVICE_ROLE_KEY or an sb_secret_ key in client code. Enable Row Level Security on every table in the public schema. Do not use a policy of using (true) for private data.
6. Limit how often the Edge Function can be called, so a stranger cannot loop it and spend the key.
7. Tell me which keys were removed, without printing the values, so I can rotate them in the provider dashboard. Deleting a key from the project does not un-leak a bundle that is already public.

Do not print any secret value.

What a Burnproof scan actually checks

The free scan reads the public HTML and up to six same-origin scripts. Scripts past that cap are unread. In that text it looks for a fixed set of shapes: OpenAI keys starting with sk-, Anthropic sk-ant-, Google AIza (a Firebase client config is reported separately and is meant to be public), Stripe sk_live_, rk_live_, test secrets, and whsec_, AWS access key ids, GitHub, Slack, and SendGrid tokens, a PEM private key, and a Supabase JWT whose payload role is service_role or anon.

It does not search for sb_secret_. That is why the DevTools search above is still worth doing. If it finds a Supabase project URL and a legacy anon key, it asks up to five tables whether they answer with no logged-in user, and it keeps a row count when the server sends one. It does not download rows. It does not send a service_role key anywhere. It requests /.env and /.git/config and only reports whether the body looks like those files. It also notes a few missing security headers. It does not call OpenAI, Anthropic, Gemini, or Stripe, and it does not read Firebase Security Rules. A quiet report is not a promise the app is safe.

Scan your app free

Paste the published URL. The free scan takes about a minute and masks anything that looks like a key. It reads the public page. It does not call the provider with a key it finds.

Scan your app free