Skip to content
Burnproof

Checklists

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

v0 defaults to Next.js. The same prefix rule applies: a variable is sent to the browser only when its name starts with NEXT_PUBLIC_. v0 says it analyzes that prefix and warns when the usage looks risky, and that it can move the call into a Route Handler, a Server Action, or other server code. The warning is about the project. It is not a search of the URL you already published. v0 security · Full-stack apps.

v0 also says you can store environment variables on the project for provider keys. Client-side names still need NEXT_PUBLIC_. A key with that prefix, or a key written into a client component so the preview runs, ships in the bundle. When the app deploys to Vercel, v0 says server code runs as Functions and that environment variables are stored in Vercel's vault. Vercel's own docs say those values are encrypted at rest, and that a variable marked Secret is write-only after you save it. Vercel environment variables.

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 to a route handler or server action

Tell v0 to call the provider from a Route Handler (app/api/.../route.ts) or a Server Action, and to read a server-only name such as OPENAI_API_KEY. Do not return the key and do not pass it into a client component. Next.js inlines NEXT_PUBLIC_ at build time, so renaming the variable is not enough until you rebuild and redeploy. Next.js environment variables.

A Server Action is still a public POST. Next.js compares the request origin to the host, and that check is not a login. Inside the action, confirm the caller is allowed to spend the key, validate the input, cap tokens, and rate-limit it. Hiding the form is not the same as hiding the endpoint. Server Actions.

Set the private key in the v0 project environment, without NEXT_PUBLIC_. Set the same name on the Vercel project as a Secret, for Production and for any preview URL you share. Vercel applies environment changes on the next deployment, so redeploy after you add the key. Do not commit the value in an env file.

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.

Enable Row Level Security on every table in the public schema before you share the link. Policies apply to the publishable key and the legacy anon key. They do not apply to a secret key. The browser may hold NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. It must not hold SUPABASE_SECRET_KEY or a service_role JWT.

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 v0 app before it is published. v0's Next.js apps inline any NEXT_PUBLIC_ value into the browser bundle. A provider key there 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 those values from client components, from any NEXT_PUBLIC_ variable, and from files under public/.
2. Keep private keys in this project's environment variables with no NEXT_PUBLIC_ prefix. Names like OPENAI_API_KEY, ANTHROPIC_API_KEY, and SUPABASE_SECRET_KEY stay on the server. NEXT_PUBLIC_ is only for values that are safe to publish, such as the Supabase URL and the publishable or anon key.
3. Move the provider call into a Route Handler or a Server Action. Read the key only in that server code. Do not pass it to a client component and do not return it.
4. Treat the route or action as public. Check that the caller is allowed to use it, validate the input, cap tokens, and rate-limit it.
5. Enable Row Level Security on every Supabase table in the public schema. Do not use a policy of using (true) for private data. Never ship a service_role JWT or an sb_secret_ key.
6. When this deploys to Vercel, set the same private names in the Vercel project's environment variables as Secret values, then redeploy. Tell me the names, not the values.
7. Rotate every key that was already in the client or in git.

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