Skip to content
Back to field notes
DeploymentJune 6, 20266 min read

AWS Amplify build-time env vars leak into client bundles — how to check

On AWS Amplify, any env var your framework treats as public is compiled into the shipped JavaScript at build time, so a mis-prefixed service key becomes readable to every visitor. This note shows how to check your own bundle before you share the URL.

DeploymentAWS AmplifySecrets

A mis-prefixed env var ends up in your shipped JavaScript

The concrete AWS Amplify environment variable client bundle risk is this: any variable your framework treats as public is inlined into the JavaScript Amplify builds and serves, so it becomes readable by anyone who opens the page. There is no server boundary in front of it. If a value named something like REACT_APP_STRIPE_SECRET or NEXT_PUBLIC_SUPABASE_SERVICE_ROLE made it into the build, the string is sitting in a file your visitors download.

This is worth separating into what is observable and what is only possible. Observable: a build-time public-prefixed variable is in the bundle if it was set during the build, full stop. That is how the framework is designed to work. Only possible: whether that exposed string is actually a live, privileged credential depends on what you put in it. A public API endpoint or a feature flag is fine to ship. A service key, database password, or signing secret is not.

The reason this matters before launch is timing. Once the production URL is shared, the bundle is public, crawlable, and cacheable. You cannot un-ship a string that is already in someone's browser cache or a CDN edge. Rotating the leaked secret is the only real remedy after the fact, and rotation is a scramble you would rather avoid.

Why this hits AWS Amplify and AI-built React apps

Two things combine here. First, the framework rule: Create React App inlines every variable prefixed REACT_APP_ into the build, and Next.js inlines every variable prefixed NEXT_PUBLIC_. These prefixes are an explicit signal to the bundler that the value is meant for the browser. Amplify just runs that build, so whatever the framework decides is public lands in the shipped assets.

Second, the Amplify build environment makes it easy to feed those variables in. You set environment variables in the Amplify console, and the build phase in amplify.yml can write them into a .env file or export them so the build picks them up. None of that is wrong on its own. The leak happens when a secret gets the public prefix, or when a build step copies the whole environment into a file the bundler reads.

AI-generated scaffolding raises the odds. When you ask a tool to wire up Stripe, Supabase, or a third-party API in a React app, the generated code often reaches for one env var name and uses it on both the client and the server without distinguishing the two. The generated amplify.yml may echo variables into a .env at build time as a convenience. The app runs, the demo works, and nobody pauses to ask which of those values is supposed to be a secret. The fast path produces a working build, not a reviewed one.

  • The public prefix is a framework contract, not an Amplify quirk. The bundler is doing exactly what the prefix tells it to.
  • AI scaffolds often blur the client and server split, so a key that should be server-only gets a browser-facing name. See server-only versus public env vars explained for how that line is drawn.
  • A build step that writes the full environment into a .env file can sweep secrets into a context the bundler reads.

Check your own bundle before you launch

You can confirm exactly what shipped without any attack, working only on your own app. Start from the deployed URL.

Open the site, then open DevTools and the Sources or Network tab. Look at the main JavaScript bundles Amplify serves. Use the in-DevTools search across all loaded sources and search for your prefix strings and your secret-shaped tokens: REACT_APP_, NEXT_PUBLIC_, and any value prefixes you recognize such as sk_, sk-ant-, sk-proj-, or the literal text service_role. If a real secret value appears as a string in the bundle, that is the suspicious result and a launch blocker.

You can also check it from the build side. Inspect your amplify.yml for a build step that writes environment variables into a .env file, and review the variable list in the Amplify console. Any variable carrying the REACT_APP_ or NEXT_PUBLIC_ prefix should be something you are comfortable printing on a billboard.

  • Suspicious result: a secret-shaped string (a service key, a private API key, a database password) is findable in the served JavaScript. It is public the moment the URL is.
  • Safer result: only values that are public by design appear, such as a publishable key, a public project URL, an analytics ID, or a feature flag. A publishable key like one prefixed pk_ is meant to be in the client; a secret key prefixed sk_ is not.
  • Repeat for each bundle and each prefixed variable. One clean file does not vouch for the next, and a value can be inlined in more than one place.

Fix it by keeping secrets off the public prefix

The fix is to stop secrets from ever getting a browser-facing name, and to keep privileged work on the server.

Audit every public-prefixed variable and demote the ones that are actually secrets. A service key, signing secret, or database credential must not carry REACT_APP_ or NEXT_PUBLIC_. Move that work to a server context the browser never sees: an API route, a serverless function, or an Amplify backend function, where the secret is read at runtime and never compiled into client assets. Then rotate any secret that was already exposed, because a value that shipped publicly should be treated as burned even if you never saw it abused.

What this fix does not cover is just as important to name. Removing a secret from the bundle does not secure the endpoint that secret was talking to. If your Supabase tables or your API still answer to a public caller, demoting the key changes nothing about that exposure; authorization on the backend is a separate job. The fix also does not retroactively protect a key that was live in a previous deploy. And it does nothing for secrets leaked through other surfaces, such as a committed .env file or an exposed source map.

  • Rename or remove any secret that carries a public prefix, then rebuild so the new bundle no longer contains it.
  • Move privileged calls behind a server route or function; the browser sends a request, the server holds the key.
  • Rotate exposed secrets and assume the old value is compromised. Closing the leak does not undo the past exposure.

Distinguish authentication from authorization here. Hiding the key controls who holds a credential. It does not decide who is allowed to do what once a request reaches your backend. A demoted key with an open endpoint behind it is still an open endpoint.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check scans your production bundle for leaked key patterns, so an AWS Amplify environment variable client bundle leak is exactly the class of problem it is built to surface. It loads the public surface the way a visitor would, then looks for secret-shaped strings in the shipped JavaScript and flags them with a severity and a fix direction. That is the same check you can run by hand in DevTools, automated against the URL you are about to share.

What it does not do is reason about your backend. The scan sees what is reachable and readable from outside; it does not know whether a flagged string is a live credential, whether the endpoint behind it is locked down, or whether your amplify.yml is sane. It is not a penetration test, a code review, or an authorization audit, and a clean scan does not make your app secure.

Next step: open your deployed site, search the bundles for your prefix and secret patterns, and demote or rotate anything that should not be public. Then run a focused Launch Check before sharing the URL publicly, so a leaked key shows up in your scan instead of someone else's.

A clean Launch Check means no secret-shaped strings were found in the served bundle at scan time. It does not prove your secrets are well-managed, that your server routes are authorized, or that a value you consider public is safe to expose. Treat a flagged string as a launch blocker, and treat a clean result as one signal among several.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.