Environment Variables and Secrets in an AI-Built App

A decision tree for where API keys and secrets belong in an AI-built app, so a generated frontend never accidentally ships a private key to the browser.

Steve Jefferson
Steve Jefferson
Developer Advocate
15 August 20261 min read

Every AI app builder eventually asks you to paste in an API key. Where that key lands, in a server-only environment variable, a build-time config value, or a client-exposed public key, decides whether it stays private or ships to every visitor's browser. Get environment variables and secrets in an AI-built app wrong once and the key is in the compiled JavaScript forever, visible to anyone who opens dev tools. This is a structural question, not a vigilance question: you fix it by knowing which of three buckets each value belongs in before you generate a line of code.

Why AI app builders make this easy to get wrong

A human backend developer keeps a mental wall between "code that runs on my server" and "code that runs in the user's browser." An AI app builder often generates both sides of that wall in the same prompt, in the same file tree, sometimes in the same file. The model is optimizing for "make the feature work," and the shortest path to a working demo is frequently a client-side component calling a third-party API directly with the key inline. It runs. It looks correct. It is also now public.

This is different from an agent accidentally committing a secret to git history, which is a leak after the fact that you clean up with rotation and a history rewrite. What's covered here is the earlier problem: designing where a secret lives in the app's architecture so it never has the chance to leak in the first place.

The three buckets every value falls into

Every configuration value in your app, secret or not, belongs in exactly one of three places. Mixing them up is the entire failure mode.

Bucket

What lives here

Who can see it

Server-only runtime env

Third-party secret keys, database URLs, service-role keys, webhook signing secrets, anything that spends money or grants write access

Only your server or serverless functions, never sent to the browser

Build-time public config

Values baked into the frontend bundle at build time, meant to be public: publishable keys, analytics IDs, feature flags

Anyone who views page source, permanently, once built

Client-exposed public keys

Vendor-issued keys explicitly designed for browser use, paired with server-side restrictions

Anyone, by design, but scoped so exposure is harmless

The confusion almost always happens between the first and third buckets, because both eventually involve a string called an "API key" sitting in an .env file. The difference is not where the value is stored, it's whether the vendor designed that specific key to be safe in public. A database connection string is a good test case: whichever database your builder wired up, its connection string always belongs in the server-only bucket, never the other two.

A decision tree for where a secret belongs

Run every credential through these questions, in order, before you let the builder wire it into either the frontend or backend.

  1. Does this value let someone spend money, delete data, or read other users' data if they get hold of it? If yes, it is server-only, full stop. This covers OpenAI and Anthropic API keys, Stripe secret keys, database connection strings, and service-role or admin keys for your backend.

  2. Is it a key the vendor explicitly labels as publishable, anon, or public, in their own documentation? If yes, and only then, it can live in a build-time public variable. Stripe's publishable key and a Supabase anon key are built for this. A Stripe secret key used for payments, or a Supabase service-role key, is not, even though both live in the same dashboard.

  3. If it's a public-by-design key from step two, have you actually configured the server-side restriction that makes it safe? A Supabase anon key is only safe with row-level security policies turned on, which matters most once you've added real user accounts and per-user data. A Google Maps browser key is only safe with domain restrictions set. A Stripe publishable key is safe on its own, since it can only create charges the account owner defined.

  4. Is it a non-sensitive setting, like a feature flag, a public app name, or an analytics ID with no write access? These can go in build-time config without a second thought.

  5. Still not sure? Default to server-only. A secret that's harder to use than it needs to be costs you a refactor later. A secret that leaks costs you a rotation, an audit, and possibly a bill.

Where this breaks in practice

Most AI app builders and frameworks use a naming convention to decide what gets inlined into the client bundle at build time. Next.js uses the NEXT_PUBLIC_ prefix, Vite uses VITE_, Create React App uses REACT_APP_. Any variable named with that prefix gets baked into the JavaScript the browser downloads, regardless of what's actually inside it. The convention is a promise to the build tool, not a security control, and an AI agent generating scaffolding code doesn't know your intent, only the pattern it's seen before.

The failure looks like this: you ask the builder to "let the app summarize documents with OpenAI," it writes a client-side component that imports OPENAI_API_KEY, and to make the build succeed it either prefixes the variable so the bundler will resolve it, or the framework's default env loading already exposes anything without an explicit server-only guard. Either way, the key ships in the bundle. Once that build is deployed, the key is public even if you later delete the code that used it, because the value is sitting in already-downloaded, cached, and possibly archived JavaScript files. Rotating the key is the only fix at that point, not editing the code.

The correct pattern is a proxy: the client calls your own server route, your server holds the secret key in a server-only variable, and your server makes the actual call to OpenAI, Stripe, or whatever third-party API needs the credential. The browser never sees anything but your server's response.

A quick way to check what actually shipped

You don't have to trust the builder's explanation. After a deploy, open the site, view source or check the network tab, and search the downloaded JavaScript files for the literal name of your secret variable, or a recognizable prefix from the vendor (OpenAI keys start with sk-, Stripe secret keys start with sk_live_ or sk_test_). If it's in there, it's public, regardless of what any dashboard setting claims.

Setting it up so this doesn't happen again

  • Never let a secret variable's name start with a client-exposing prefix (NEXT_PUBLIC_, VITE_, REACT_APP_, or whatever your framework uses), even temporarily for testing. Naming discipline is the cheapest control you have.

  • Keep a real .env file for local secrets, gitignored, and a checked-in .env.example with placeholder values so the AI builder and any collaborator know what variables exist without seeing the values.

  • Store production secrets in your hosting platform's own secret manager (Vercel, Netlify, Supabase, Railway, and similar all have one), not hardcoded in a config file that gets committed or copied between environments.

  • Use separate keys for staging and production, and for a payments integration specifically, use test-mode keys until launch. A leak or a bug in staging can't touch live data or a live charge if the keys were never shared.

  • After any significant AI-generated feature that touches a paid API, search the generated frontend code for the vendor's client library or a raw fetch call to the vendor's domain. If you find one outside a server route, that's a proxy you still need to build.

  • If you disconnect an AI app builder from a project, stop using one, or hand the project off to a developer, rotate any key the builder or the previous owner had access to. Treat builder-managed secret storage the same way you'd treat a former contractor's access.

FAQ

What's the difference between a secret and a public environment variable?

A secret grants access, spending power, or data visibility if someone else obtains it. A public environment variable is a value the vendor designed to be safe in a browser, like a publishable key paired with server-side restrictions, or non-sensitive config like a feature flag. The test isn't where the value is stored, it's what happens if a stranger reads it.

Is it safe to put an API key in a .env file in an AI app builder?

The .env file itself is fine as a mechanism, that's what it's for. What matters is whether the variable name uses a client-exposing prefix and whether the builder's generated code reads it from a server-side context or a browser-side one. The same key in the same file can be safe or exposed depending entirely on how the generated code consumes it.

Can I use the same secret in staging and production?

You can, but it removes your safety margin. If a staging environment gets misconfigured, has looser access controls, or is where you let an AI agent experiment more freely, a shared key means that mistake reaches production data or a production bill. Separate keys per environment cost a few minutes to set up and contain damage when something goes wrong.

What happens if my AI builder bakes a secret into the frontend build?

The key is public the moment that build is deployed, and stays public in any cached or archived copy of that JavaScript even after you fix the code. Rotate the credential at the vendor immediately, then fix the code to read the secret server-side instead. Editing the source without rotating the key does nothing, the already-shipped bundle still has the old value in it.

Do I need a secrets manager for a small AI-built app?

Not a dedicated third-party one. The environment variable settings built into Vercel, Netlify, Supabase, Railway, and most hosting platforms are a secrets manager for this purpose: encrypted at rest, scoped per environment, not visible in your repository. A standalone secrets manager becomes worth it once you have many services, many environments, and need audit logs of who accessed what.

The core rule to keep

Every variable answers one question before it goes anywhere: does exposing this cost money, data, or access? If yes, it's server-only. If a vendor explicitly built it for the browser and you've configured the restriction that makes that safe, it can be public. Everything else is a judgment call, and the safe judgment call is server-only. If you're earlier in the process and still deciding how the rest of the app should be structured, how to build an app with AI is the starting framework this decision tree slots into.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.