Environment variables and secrets: where your app's keys should live

Which values belong outside the code, which keys may go in the browser and which may not, and what to check when an AI writes your app's configuration.

info Quick Definition
Environment variables are configuration values stored outside the code, such as the address of a database or a service key. A secret is the subset that grants access: if someone obtains it, they can act as your app. Keeping both out of the code is one of the most basic security measures.

What goes in the code and what goes outside?

The Twelve-Factor App methodology, a classic guide for modern applications, defines config as everything that can vary between deploys: database addresses, credentials for external services and the server’s hostname, for example. It recommends storing config in environment variables for three reasons: they change without touching the code, there is little chance of committing them to the repository by accident, and they are a standard that does not depend on language or operating system. Its litmus test: the repository should be publishable at any moment without compromising any credential (Twelve-Factor App, Config).

What does not change between deploys, such as the app’s internal route structure, can live in the code. The practical rule: if it changes by environment or grants access to something, it goes outside.

Environment variables are the minimum, not the ceiling. OWASP’s guide warns, in its section on containers, that they are generally accessible to all processes and may end up in logs or system dumps, and it prefers injecting the secret as a file or in memory (OWASP, Secrets Management Cheat Sheet). Stripe says something similar: use your platform’s secrets vault and, if there is none, environment variables (Stripe, API keys). For a small website, the variables your deployment platform offers are a reasonable starting point; writing the key in the code never is.

What is a secret, and which key can go in the browser?

A public value, such as the app’s name, can be seen without a problem. A secret, such as the key for a payment API, cannot: whoever holds it can make calls on your behalf, use up your quota or read data. That is why platforms such as Vercel distinguish two types when you save a variable: config values, which stay readable to people with access to the project, and secrets, which after saving can only be replaced, not read (Vercel, Environment variables).

The key question is whether the value travels to the browser. In Next.js, variables without the NEXT_PUBLIC_ prefix exist only on the server; those with it are copied into the JavaScript the browser receives, at the moment the app is built (Next.js, Environment variables). Putting a secret key behind that prefix is the same as publishing it.

Not every key is secret. Stripe explains it well: its publishable key (pk_) is safe to expose and is meant for browser code and mobile apps; secret keys (sk_) and restricted keys (rk_) are not, and must live only on the server (Stripe, API keys). Each provider has its own rule, so before putting a key in the browser, check in its documentation that this specific key is public. If in doubt, treat it as secret.

How do you handle environments: production, preview and local?

The same variable can have different values per environment. On Vercel you can define variables for production, for preview (any branch other than the production one) and for local development; a variable specific to a branch overrides the general preview one (Vercel, Environment variables). That way previews use a test database and only production uses the real one.

OWASP is on the same line: the secret should be different in each environment, and production secrets should be kept apart from development ones. Using the production key in your tests is the opposite mistake.

Two practical details. A change to a variable does not affect existing deployments: it applies to the next one, so you must deploy again. And in GitHub Actions, secrets are stored per repository, environment or organization, and they are not handed to the runner when a workflow is triggered from a forked repository, except for the GITHUB_TOKEN (GitHub, Using secrets in GitHub Actions).

What should you check when an AI writes the configuration?

When an agent creates a configuration file or a form that calls a service, check five things:

  1. That the key does not appear written in a code file. It must be read from a variable.
  2. That the example file has no real values. An .env.example shows only names and hints, with placeholders such as YOUR_KEY_HERE.
  3. That the browser key is a public one. Depending on the provider, some keys are meant for the client and others are not (see above).
  4. That the repository ignores local configuration files. Next.js warns that you almost never want to commit .env files, and its starter template already adds them to .gitignore (Next.js).
  5. That the agent does not read what it does not need. Claude Code lets you block reading with deny rules, for example Read(./.env); according to its documentation, those rules cover its file tools and recognized shell commands such as cat, but not a script that opens files on its own. For operating-system-level blocking you need to enable the sandbox (Claude Code, Permissions).

One more habit: do not paste a real key into a chat or a prompt.

What do you do if a key leaks?

Suppose a key was pushed to a repository or showed up in a screenshot. The order matters:

  1. Revoke or rotate it at the service that issued it. GitHub presents this as the first step: once a secret is revoked or rotated, it can no longer be used for access. Rewriting the repository history may not be necessary, and it does not reach other people’s copies such as forks or clones (GitHub, Removing sensitive data from a repository). OWASP also calls for immediate revocation of exposed keys.
  2. Put the new key on your platform and deploy again. Vercel recommends generating the new one, updating it in the project, redeploying, checking that it works and only then invalidating the old one, so the app is not left broken (Vercel, Rotating environment variables).
  3. Check the provider’s logs, if it offers them, in case someone used the old key.

To prevent it, GitHub blocks pushes that contain secrets to public repositories from your account by default (user push protection), while repository push protection has to be turned on (GitHub, Push protection).

Example

Illustrative example: a designer uses AI to generate a page that shows the weather for an event. The AI writes the weather API’s key directly in the page file. She spots it by searching the project folder for the words key and token before publishing. She asks the agent for three changes: read the key from an environment variable, make the call from a server function rather than from the browser, and leave only the line WEATHER_API_KEY=YOUR_KEY_HERE in .env.example. Because the key was already written in a file that could have been copied or pushed, she generates a new one in the provider’s dashboard, stores it as a secret on the deployment platform, deploys again and revokes the old one.

Common mistakes

  • Writing a key directly into the page’s code.
  • Uploading a .env file with real values to the repository.
  • Using the production key in tests or previews.
  • Believing that deleting the key from the code is enough if it was already published: it must be revoked and replaced.
  • Putting a secret key in a variable with a public prefix, such as NEXT_PUBLIC_.
  • Changing a variable and not deploying again.
  • Invalidating the old key before deploying the new one, leaving the app without service.

To go further: reading code without coding, deploy for designers, what vibe coding is and Claude Code.

Sources

Frequently asked questions

What is an environment variable?
It is a key-value pair stored outside the code, which the app reads when it runs. It lets values change between environments, such as the test or production database, without touching the code.
What is a secret in an app?
It is a value that grants access to a service, such as an API key, a token or a password. If someone obtains it, they can use the service as if they were the app.
Why must a secret key not be in browser code?
Because anyone who opens the page can read the code that reaches the browser. A secret key there is effectively a public key. Only keys the provider designed for that purpose belong in the browser, such as Stripe’s publishable key.
What happens to variables when I change their value?
On platforms such as Vercel, the change applies to new deployments, not to existing ones. You must deploy again for the app to use the new value.
What should I do if I committed a key by mistake?
First revoke or rotate it at the service that issued it: that stops it working even if it stays in the repository history. Then replace it on your platform and deploy again.