Skip to content
Burnproof

Checklists

Before you publish your Bolt 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 Bolt 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

Bolt documents three Supabase names: VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY. The first two use Vite's public prefix. Vite says every VITE_ value is exposed in client source after bundling, and that the prefix should not be used for secrets. The anon key is meant to be public. A provider key in any VITE_ name is in the bundle a visitor downloads. Vite env variables · Bolt databases.

Secrets are for server functions, which Bolt also calls edge functions. Bolt says a server function reads the secret and that the value is not sent to the visitor's browser. Open the database icon and click Secrets, or use the gear menu when the project shows secrets there. Bolt says that asking it to integrate OpenAI makes the agent ask you for the secret. Still search the published bundle. The prompt does not prove the client code is clean. Secrets settings.

Sharing is a separate leak. Bolt says Editors and Co-owners can view environment variables, and Viewers cannot. That rule is about the editor. It does not hide a VITE_ value from someone who opens the published URL. Share your project.

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 a server function

New Bolt projects use a Bolt database on every plan. That database has server functions and a Secrets tab. Ask Bolt to call OpenAI, Anthropic, Gemini, or Stripe from a server function that reads the secret by name. The client calls the function.

Supabase is the other path, documented for Pro and Teams plans. Bolt says you can prompt it to create an edge function, for example to handle a conversation with OpenAI, after the Supabase account is connected. Store that key in the Supabase project's secrets, and keep it out of VITE_ variables. Supabase for databases · Server functions.

A public function with no limit is still your meter. Ask for a check that the caller is allowed to use it, and a limit on how often it runs.

Row level security

Bolt says anyone who visits the site can send requests to the database without using your screens. Row level security is what decides which rows those requests can touch. Bolt says it turns RLS on for tables it creates, and that a table without RLS can be read and changed by anyone. If you add a table yourself, turn RLS on for that table too.

On a site with no sign-in, Bolt says it may write policies that let any visitor view, add, change, and delete rows. The database Security tab flags those as RLS Policy Always True. That warning is acceptable only when you want every visitor to change that data. Private rows, such as orders or messages, need sign-in and a policy limited to the signed-in user. Open the Security tab, or run the database security check, which Bolt says is on every plan. Database 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.

Paid plans also have a project security audit under Publish, then Run security audit. Bolt says that audit checks that private keys and passwords are not stored in the project code or sent to visitors' browsers, and that it does not spend tokens when you use that button. Prompting the chat to audit does spend tokens. The documented daily limit is 30 audits, resetting at midnight UTC. The audit reviews the project. After you publish, search the live URL anyway. Project security audit.

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.

If you keep a local .env for secrets, do not commit it. Bolt's Secrets tab is the store for server functions. A committed env file that also holds VITE_ values will publish those values, which is safe only when every value in the file is meant to be public.

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 Bolt 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 client components and from any VITE_ variable.
2. Vite exposes every VITE_ value in the client bundle. VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY (or a publishable key) may stay, because those values are meant to be public. Do not put a provider key or SUPABASE_SERVICE_ROLE_KEY behind a VITE_ prefix.
3. Move each provider call into a server function (edge function). Read the key from the project Secrets store, not from client code. If this project uses Supabase, put the call in a Supabase Edge Function and store the key in that project's secrets.
4. Enable row-level security on every table. Bolt turns it on for tables it creates; check tables that were added by hand. Replace any policy Bolt flags as RLS Policy Always True unless that table is meant to be world-writable. Private rows need sign-in and a policy that matches the signed-in user.
5. Limit how often the server function can be called, so a stranger cannot loop it and spend the key.
6. Tell me which keys were removed, without printing the values, so I can rotate them. 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