Cobra Developers
Blog, Infrastructure

Environment variables and secrets: keeping keys out of your code

The short answer

An environment variable is a named value supplied to a program by the system it runs on, instead of being written in the code. Secrets such as API keys and database passwords belong in environment variables stored by your hosting platform, never in the repository, with separate values for test and live, and immediate rotation if one is exposed.

CoverageCobra, our marketplace for attorneys, talks to a payment provider, a text message provider, an email provider, an error tracker, and a database. Each of those hands us a key, and each key is the same thing as a password. Anyone holding the payment key can move money. Anyone holding the email key can send mail as us.

Our internal Firm Mail Intake tool is a sharper case. It reads a law firm's incoming email and files attachments, unattended. The credentials that let it do that are about as sensitive as credentials get.

None of those values appear anywhere in our code. This post explains where they live instead, and the rules we follow.

What is an environment variable?

An environment variable is a named value that the operating system or hosting platform hands to a program when it starts. The code asks for the value by name instead of containing it. The same code can then run with different settings on a laptop, a test server, and production.

In a Node application, reading one looks like this:

const apiKey = process.env.PAYMENTS_SECRET_KEY;
if (!apiKey) throw new Error('PAYMENTS_SECRET_KEY is not set');

The code knows the name. It does not know the value. On a developer's machine the value usually comes from a local file named .env. In production it comes from the hosting platform's settings screen, where it is stored encrypted and injected when the app runs.

Environment variables hold ordinary configuration too, such as which address the app is served from. The ones that need care are the secrets.

What counts as a secret?

A secret is any value that grants access to something: an API key, a database connection string, a password, a signing key, a webhook signing secret, or a login token. If a stranger holding the value could read your data, spend your money, or act in your name, it is a secret.

Not every key is secret, which confuses people. Some services issue two kinds:

Kind of valueExampleSafe in the browser?
Publishable or public keyA key that only identifies your account to a payment formYes, it is designed to be seen
Secret keyA key that can create charges or read recordsNo, server only
Database connection stringAddress, user, and password in one lineNo, server only
Webhook signing secretProves an incoming message is from the providerNo, server only
Site address, feature switchesThe public URL of the appYes, not a secret at all

The provider's documentation will say which is which. When it is not clear, treat the value as secret.

Where should secrets be stored?

Secrets should be stored in the environment variable settings of your hosting platform or in a dedicated secrets manager, and in a local .env file on each developer's machine that is excluded from version control. They should never be in source code, chat messages, shared documents, or screenshots.

The local file needs one line of protection. A .gitignore entry tells Git never to track it:

# .gitignore
.env
.env.local

A useful companion is a file named .env.example that does go in the repository. It lists every variable name the app needs, with empty or fake values. A new developer copies it, fills in their own values, and knows nothing is missing.

# .env.example
DATABASE_URL=
PAYMENTS_SECRET_KEY=
EMAIL_API_KEY=

That gives you the documentation without the danger.

What are the rules for handling secrets?

The rules for handling secrets are short: keep them out of the repository, keep them out of the browser, separate test from live, give each key the least access it needs, and replace any key the moment it may have been seen. Following them costs almost nothing. Skipping them is how accounts get drained.

  1. Never commit a secret. Not in code, not in a config file, not in a comment. Add .env to .gitignore before the first commit.
  2. Never send a secret to the browser. Anything shipped to the browser can be read by anyone who opens the developer tools. Frameworks mark browser-visible variables with a special prefix, such as NEXT_PUBLIC_ in Next.js. Do not put a secret behind it.
  3. Use separate test and live keys. Payment and messaging providers issue both. Development and preview environments get test keys only, so a bug cannot charge a real card or text a real customer.
  4. Give each environment its own values. Production, preview, and local development should not share a database password.
  5. Grant the least access that works. If a provider lets you create a restricted key that can only do one job, use it.
  6. Rotate on exposure, immediately. If a key was pasted in a chat, committed by accident, or left on a departed contractor's laptop, create a new one and revoke the old one. Do not weigh whether anyone saw it.
  7. Do not log secrets. Check that error reports and request logs do not print headers or connection strings.
  8. Keep a list of what exists. Know which keys you have, what each can do, and where each is used, so rotation is a ten minute job and not an investigation.

What should you do if a secret is committed to Git?

If a secret is committed to Git, revoke it at the provider and issue a new one first. Deleting the line in a later commit is not enough, because the old value stays in the repository's history, and anyone with a copy of the repository still has it.

The order matters:

  1. Revoke and replace the key in the provider's dashboard.
  2. Update the new value in your hosting platform and redeploy.
  3. Check the provider's activity log for use you do not recognize.
  4. Then clean up the repository if you want the old value gone from history.

A rotated key is harmless wherever it still appears. An unrotated key is dangerous no matter how well you scrubbed the history. This is also a good reason for the business owner to hold the provider accounts directly, a point we make in who owns your website.

Why do test and live keys need to be kept apart?

Test and live keys need to be kept apart so that unfinished code can never affect real customers or real money. A test key talks to a sandbox where charges are pretend and messages go nowhere. A live key is the real thing. Mixing them turns an ordinary bug into an incident.

On a platform with preview deployments, every proposed change gets its own running copy of the app. Those copies should carry test keys and a non-production database. Only the production environment holds live values. We describe that split in staging vs production, and why money-handling code deserves it in how to add payments to a web app.

Frequently asked questions

Is a .env file secure?

A .env file is as secure as the machine it sits on. It is fine for local development if it is excluded from version control and holds test values. It is not a place for production secrets, which belong in your hosting platform's encrypted settings.

Can I put an API key in front-end JavaScript?

Only if the provider describes it as a publishable or public key meant for browsers. Any secret key in front-end code is visible to every visitor. Calls that need a secret key should go through your own server, which adds the key.

How often should secrets be rotated?

Always after any possible exposure and whenever someone with access leaves. Beyond that, a regular schedule is good practice, and it is far easier if you have kept a list of your keys.

Is a private repository a safe place for secrets?

No. Private repositories get cloned to laptops, shared with contractors, and connected to outside tools, and each copy carries the full history. Treat the repository as something other people will read, and keep secrets out of it.

If you have inherited an app and are not sure where its keys live or who else has them, we can help you find out. Contact us.

Who wrote this

The Cobra Developers team. We are a software studio in Arizona that builds and runs its own products and builds websites, web apps, and automation for clients. This post draws on our work on CoverageCobra, Firm Mail Intake. Everything here comes from projects we shipped; see all 30 of them.

guest@cobra: ~