A frontend bundle is a file the browser downloads. Anything in it's readable by the person using the site, and by anyone else who opens the same URL. That's just how web pages work, and the browser is doing its job. The useful question is which keys were designed for that, and which keys were not.
Vendors answer the question in their own docs. I pulled those answers together here for the keys that show up in AI-built apps, then laid out what Deploy Safely's free check actually pattern-matches. It doesn't find every kind of credential. So a quiet scan isn't a promise that no key is public.
Stripe: one key is for the browser
Stripe's API keys page draws the line in a table. The publishable key, pk_, is safe to expose. It identifies your account and can create tokens or PaymentMethods from payment details. It can't create charges or read account data. Stripe says you can include it in front-end code. The secret key, sk_, isn't safe to expose. It has unrestricted permissions, and Stripe recommends restricted keys (rk_) for new server-side use so a leaked key can do less. Restricted keys are also not safe to expose. Test keys use the same prefixes with _test_ in the middle. A test secret in a public bundle is still a secret. Stripe's best practices say secret keys must stay in the server environment, and that you shouldn't put them in source code, client apps, or mobile apps.
If the chat said "add checkout" and the button works, look at which key the browser received. pk_ in the page is normal. sk_ or rk_ in the page means someone can call the API as you. A webhook signing secret (whsec_) is also a server value. It's how your server knows a webhook came from Stripe. It doesn't belong in the client either.
OpenAI: the key stays on your server
OpenAI's help center article, Best practices for API key safety, says never to deploy a key in client-side environments like browsers or mobile apps. Their reason is simple: someone can take that key and make requests on your behalf. They tell you to route requests through your own backend and to put the key in an environment variable named OPENAI_API_KEY. The production best practices guide says the same thing in different words: don't expose keys in code or in public repositories. Store them in environment variables or a secret manager, and set an expiration when you create a project key.
There's no publishable OpenAI key that is meant for a browser in those two pages. A chat box that calls the OpenAI API from a React component, with the key in the component or in a VITE_ variable, is the pattern those pages tell you not to ship. The call belongs on a server route that reads the environment variable. After the key has been in a public bundle, rotating it's your job in the OpenAI dashboard. Removing it from the next build doesn't undo the copy that already loaded.
Anthropic: browser use is off on purpose
Anthropic's TypeScript SDK docs say browser use is disabled by default so you don't expose secret API credentials. Turning it on means setting dangerouslyAllowBrowser to true. The same page says that option can be dangerous because any user who can use the browser can inspect and copy the key. The SDK's default is to read ANTHROPIC_API_KEY from the environment, which is a server-side habit. Get your API key says the full key, which starts with sk-ant-, is shown once at creation and should go in a secrets manager.
If a builder imported @anthropic-ai/sdk into a client component, that's the shape to undo. Call Claude from a server route. Don't flip the dangerous browser flag so the demo runs in the preview. App Attest, which Anthropic documents for iOS and macOS, is a different design for apps that call the API from a device without shipping a key. A normal website still wants a backend.
Supabase and Firebase are the exceptions people mix up
Supabase says the publishable key (sb_publishable_) and the legacy anon key are safe to expose, because they only reach what row-level security allows. The secret key and the service role key bypass row-level security and must not be in the browser. Seeing a long eyJ token in a Supabase app is normal when its role is anon. It isn't normal when its role is service_role. The longer version is how to check if your Supabase anon key is exposed.
Firebase says API keys for Firebase services are public by design and are safe to include in code when they're restricted to Firebase APIs and your rules are the real lock. A service account file isn't that key. The rules article is Firebase security rules mistakes in AI-built apps.
Treat "it's an API key, so hide it" and "it's an API key, so it's fine" as both wrong. Read the vendor's page for that prefix.
How a bundler publishes a variable
The key usually lands in the bundle because of a prefix, not because someone wanted to publish it.
Vite exposes variables that start with VITE_ in client source. The docs say those variables shouldn't contain sensitive information such as API keys, because the values are bundled at build time. A variable without that prefix, such as DB_PASSWORD in their example, isn't exposed. Their advice for a real secret is a backend or an edge function.
Next.js says non-NEXT_PUBLIC_ variables are available only in the Node.js environment. A NEXT_PUBLIC_ variable is inlined into the JavaScript sent to the browser, frozen at next build. The default create-next-app template gitignores .env files, and the docs say you almost never want to commit them. A secret in .env.local stays off the client only if nothing copies it into a NEXT_PUBLIC_ name or into a client component.
Lovable's secrets docs make the same split for their stack: VITE_ values are browser-exposed and live in .env, and backend secrets such as OPENAI_API_KEY or STRIPE_SECRET_KEY belong in the secrets manager, not in a VITE_ name. That page is linked from the Lovable guide. Bolt's secrets page says server-function secrets shouldn't end up in the browser, and Vite's prefix rule still applies if the project uses VITE_. The Bolt guide and the Cursor guide walk through those stacks.
What the free check looks for
The check reads the public page and the JavaScript that page loads. It looks for a fixed set of shapes. It doesn't understand every vendor.
In that set, an OpenAI-style secret (sk- or sk-proj- followed by a long token) and an Anthropic-style secret (sk-ant-) are critical findings. A Stripe secret or restricted key (sk_ or rk_ with live or test) is critical. A Stripe webhook secret (whsec_) is high. A Stripe publishable key (pk_) is an info note that says this is normal. A Supabase JWT with role service_role is critical, and the check doesn't use it. A Supabase JWT with role anon is an info note that the public key is present, which is expected. A database URL that includes a password is critical. The check also looks for a few other shapes: AWS access key ids, GitHub tokens, Slack tokens, a private key header, and a Google API key. The Google key is low severity, because Firebase and Maps apps often ship one, and the note says to restrict it. The check doesn't save the full secret. It keeps a short hint of the shape.
It doesn't flag an arbitrary string named API_KEY unless the value matches one of those shapes. It doesn't flag sb_secret_ as its own rule. Search for that prefix yourself. It doesn't log into OpenAI, Stripe, or anyone else, and it doesn't call the API with a key it finds.
The same pass requests /.env, /.env.local, /.env.production, /.git/config, /.git/HEAD, and /.npmrc, and it notes missing security headers when two or more of the watched headers are missing on the main response. A key that lives only in a server environment, and is never written into HTML or JavaScript, is outside what this check can see. That's the arrangement you want. The checklist is the hand version of the same pass.
If you find a real secret in the bundle, rotate it before you celebrate the next deploy. The old value is already on machines that loaded the page. The prompt can remove it from the project. It can't click the rotate button in the vendor's dashboard.
A prompt you can paste
Paste this into the builder, publish again, and run the check a second time. If a secret was already on the live site, rotate it in that vendor's dashboard. The prompt cannot do that part.
Remove secret API keys from client code. Keep only the keys that vendor docs say are safe in a browser. 1. Search client components and any VITE_ or NEXT_PUBLIC_ variable for OpenAI keys (sk- or sk-proj-), Anthropic keys (sk-ant-), Stripe secret or restricted keys (sk_ or rk_), Stripe webhook secrets (whsec_), a Supabase service_role JWT, an sb_secret_ key, a private key, and a database URL with a password. Remove them from the client. 2. Stripe's publishable key (pk_) may stay. Supabase's anon key and sb_publishable_ key may stay. A Firebase web API key may stay if it is restricted to Firebase APIs. 3. Move OpenAI, Anthropic, and Stripe secret calls to a server route or edge function that reads the key from a server environment variable. Do not return the key, process.env, or a secret in any response. 4. Do not commit .env files. Do not prefix a secret with VITE_ or NEXT_PUBLIC_. 5. List the providers whose keys were in client code so I can rotate them. Do not print the key values. Do not call the provider from the browser to make the demo work.