8 min read

Secrets in AI-generated apps: moving keys out of the client bundle

AI app builders often wire provider keys straight into browser code. Here is how to classify credentials, move secret-bearing calls server-side, rotate what shipped and stop it from coming back.

Secrets in AI-generated apps often end up in the client bundle because the generator wires the provider call right where the button is. The fix is structural: move every secret-bearing call behind a server route, keep only publishable identifiers in the browser, rotate anything that shipped, and add a build check so it cannot return.

Why secrets end up in the browser

AI app builders and coding agents take the shortest path to a working feature. If a component needs data from a model provider, a payment service or a database admin API, the fastest code calls that service directly from the component. It works in the preview, so nothing prompts anyone to change it.

Frameworks make the mistake easy. Many expose any variable with a public prefix to the browser at build time, and a generated setup guide may tell you to add that prefix so the value becomes available. Once it does, the value is embedded in JavaScript that every visitor downloads, caches and can read in developer tools.

Some keys are designed to be public: a publishable payment key, a web SDK configuration for a backend-as-a-service, an analytics ID. The problem is that generated code rarely distinguishes those from secret keys, service-account credentials or admin tokens. They all look like long strings in a configuration file.

What teams often get wrong

  • Assuming a value is safe because it lives in an environment file. Environment files control where values are read from, not who can see them after the build.
  • Deleting the key from the code and redeploying without rotating it. The old bundle may be cached by CDNs, browsers and crawlers, and the key remains in repository history.
  • Hiding the key with obfuscation or encoding. Anything the browser can decode, an attacker can decode.
  • Moving the call to a server route but leaving the route open, so anyone can use your server as a free proxy to a paid API.
  • Treating database rules as optional because the frontend only shows each user their own data.

The last two matter as much as the first. Moving a secret server-side protects the key. It does not protect the capability the key grants, and that capability is what an attacker actually wants.

A practical architecture

Classify every credential, then route by class. A short table in the repository listing each credential, its class and where it is used is enough to start.

  • Publishable identifiers such as web SDK configuration, publishable payment keys and analytics IDs can stay in the browser, protected by provider-side controls such as allowed domains, security rules and app attestation where available.
  • Secret keys and tokens for model providers, payment secret keys, CRM tokens and email sending live only in server code: API routes, server actions, cloud functions or a worker.
  • Privileged credentials such as service accounts and database admin keys live only in the server runtime, ideally in a managed secret store, and never in the repository.

The browser calls your own endpoint. That endpoint authenticates the user, checks what they are allowed to do, enforces limits, calls the provider with the secret and returns only the fields the interface needs. One server module per provider keeps those calls in one place, which also makes retries, timeouts and logging consistent.

Where the secret itself lives

For small projects, the hosting platform's encrypted environment settings are usually enough, marked as sensitive so values are not readable after entry. As the number of services grows, a managed secret store with a small server helper that reads the environment first, falls back to the store and caches in memory gives you one source of truth and makes rotation a single change instead of a hunt.

Implementation considerations

Start by finding everything. Build the production bundle and search the output for key prefixes, provider hostnames and long random-looking strings. Search the repository history too, not just the current files. Then list every place the browser talks to a third-party service directly.

Rotate before you refactor. If a secret shipped to the browser, assume it is compromised: issue a new key, revoke the old one and review the provider's usage logs for activity you do not recognize. Refactoring first leaves the exposed key live for however long the refactor takes.

When you move calls to the server, add authentication and rate limits at the same time. A route that proxies a model provider without a per-user limit turns a leaked capability into an open bill. Return narrow responses as well; generated code often passes the provider's full payload through to the client, including metadata the user never needed.

Add guards that fail the build. Mark secret-reading modules as server-only so an accidental import into a client component breaks compilation instead of shipping. Add a secret scanner to the repository and to continuous integration. Keep service-account files and local environment files out of deploy uploads and out of version control with ignore lists.

Plan the rotation itself. Keep a short runbook per provider: where the key is stored, which services read it, how to issue a new one and how to confirm the old one is revoked. When a key leaks at an inconvenient moment, the runbook turns a stressful afternoon into a routine change, and it makes it possible for someone other than the original developer to do it.

For mobile apps, remember that anything inside the app binary is public too. The same rule applies: the app calls your backend, and the backend holds the secret. Obfuscation in a mobile build slows down casual inspection; it does not protect a key.

Trade-offs

Server-side calls add latency and a component to operate. For streaming AI responses you need a route that streams through rather than buffering, which is slightly more work than a direct browser call, and you now own its uptime.

Short-lived tokens are a middle path for some providers: the server issues a scoped, expiring token and the browser uses it directly, for example for real-time voice sessions or direct file uploads. That reduces load on your server, but the scope and expiry must be right, and not every provider supports it.

A managed secret store adds cost and a dependency on the cloud account's identity setup. For a small app with three keys, encrypted platform settings plus a written rotation checklist can be the more honest choice than infrastructure nobody will maintain.

Lessons from ImadDhin work

These are code-level observations from the ImadDhin portal, not claims about incident counts or client systems.

The portal resolves server API keys through one helper that reads the runtime environment first, then a managed secret store through the server's service identity, and caches values in memory for the life of the instance. Keys are mirrored into the hosting platform as sensitive values, while public identifiers stay in public configuration. That split keeps the question of whether a value is safe in the browser answerable one value at a time.

Browser features that need third-party data go through the site's own routes. The agent workspace's research features call a server route that checks the visitor's plan before using the provider, and brand logo lookups go through a server route rather than the browser holding the vendor key. Credential files and local environment files are excluded from command-line deploy uploads by an explicit ignore list.

One cheap guard is worth naming for every codebase. A secret helper that relies on convention, or on a server-only dependency, catches an accidental client import later than it should; an explicit server-only marker makes that import fail the build immediately. It is a small change, and we recommend it in every rescue.

Common mistakes to test for

  • Search the production build output for key prefixes and provider hostnames after every release, not once.
  • Call each server proxy route without a session and confirm it refuses.
  • Call it rapidly from one account and confirm limits apply.
  • Check that error responses do not echo provider error bodies containing request metadata.
  • Confirm a missing secret produces a clear server-side error and a safe user message, not a crash page that prints configuration.
  • Verify old keys are revoked at the provider, not just replaced in your settings.
  • Check repository history, public forks and preview deployments for committed environment files.

When a simpler solution is better

If a provider offers a genuinely publishable key with domain restrictions and usage caps, and the capability it grants is low risk, keeping that call in the browser is fine. Not every integration needs a proxy, and adding one where it is not needed creates code to maintain.

If a feature only exists for a demo, remove it rather than hardening it. And if the whole app is a prototype nobody outside the team uses, rotate any exposed key, stop calling paid providers from the client and defer the full architecture until you decide the product is going to production.

Make secrets boring again

Secrets handling should be dull: one place to read them, one place to rotate them and no way to ship them to a browser by accident. For the wider hardening sequence, see turning a Lovable or v0 prototype into a production application. For agents that act with credentials, the limits of vibe-coded agents covers scoped identities and approval gates. If your AI-generated app has keys in the bundle today, vibe-code rescue starts with exactly this inventory, and a 30-minute call is enough to scope it.

Frequently asked questions

Is a backend-as-a-service public key a secret?

Usually not. Web configuration and public anonymous keys are designed to ship to the browser, and protection comes from security rules or row-level policies. Service-role, admin and server keys are secret and must never reach client code.

I removed the key and redeployed. Am I safe?

Not yet. Old bundles can remain cached and the key stays in repository history. Rotate it at the provider, revoke the old value and review usage logs for unfamiliar activity.

Do environment variables with a public prefix leak to the browser?

Yes. Frameworks inline variables with public prefixes into client JavaScript at build time. Use that prefix only for values that are safe for every visitor to read.

Can I keep a secret key in a mobile app if I obfuscate it?

No. Anything inside an app binary can be extracted. Have the app call your backend and keep the secret there, or use short-lived scoped tokens issued by your server.

How do I stop someone abusing my server proxy route?

Require authentication, apply per-user rate limits, validate inputs, return narrow responses and set spending caps at the provider where they exist.

Get keys out of the browser for good

We will walk through where your credentials live and what to rotate first.

Book a 30-minute call

Secrets, authorization, payments and monitoring for AI-built apps.

See vibe-code rescue

Keep reading