Quick Answer

Keep secrets out of code entirely — use environment variables locally and a secrets manager in production. If a key has ever been committed, rotate it. Removing it from a later commit does not remove it from history.

Why this specifically matters

Public repositories are scanned continuously by automated tools looking for credential patterns. A committed cloud key is typically found and used within minutes, most often to run up compute charges. Students have received five and six figure bills this way.

Private repositories are safer but not safe. Everyone with access has the key, it appears in every clone, it is in every backup, and it survives long after the person who added it has left.

The critical property is that git history is permanent. Committing a key and deleting it in the next commit leaves it fully readable in the earlier commit. The repository has not forgotten; the working tree has simply moved on.

Which leads to the only correct response to an exposed key: rotate it. Revoke it and issue a new one. Cleaning history is optional tidying; rotation is the actual fix.

Environment variables and .env files

The standard approach is to read secrets from the environment, so they never appear in code:

import os
API_KEY = os.environ["API_KEY"]           # fails loudly if missing
DEBUG   = os.environ.get("DEBUG", "0")    # optional with default

Using os.environ[...] for required values is deliberate — the application refuses to start rather than running with a missing credential and failing confusingly later.

For local development, a .env file holds them:

API_KEY=sk_test_abc123
DATABASE_URL=postgres://localhost/dev

Two things must accompany it. Add .env to .gitignore before creating the file, since adding it afterwards does not untrack an already-committed one. And commit a .env.example listing the required variable names with dummy values, so a new contributor knows what is needed.

In production, set variables through the platform's own configuration rather than shipping a .env file.

What to do when it has already happened

In order, and the order matters:

  1. Rotate the credential immediately. Revoke the old one at the provider. Do this first — before cleaning history, before telling anyone. Every minute the key remains valid is exposure.
  2. Check for unauthorised use. Cloud billing, API logs, database access records. Assume it was used until you have evidence otherwise.
  3. Remove it from the code and switch to environment variables.
  4. Optionally rewrite history with a tool such as git-filter-repo. This is disruptive — it changes every commit hash and requires everyone to re-clone — and it does not help if the repository was ever public or forked.

Step 4 is genuinely optional. Once a key has been exposed, it is compromised regardless of whether you scrub it. Rotation is what removes the risk; history cleaning removes embarrassment.

Prevent recurrence with a pre-commit hook that scans staged changes for credential patterns. GitHub also scans public repositories and notifies some providers directly, which is occasionally how people find out.

Secrets managers

Environment variables are fine for a small project and have real limits at scale: no audit trail of who read what, no rotation, and no easy way to grant one service access to one secret.

A secrets manager — AWS Secrets Manager, HashiCorp Vault, or a platform's built-in store — addresses those. The application authenticates to the manager and fetches secrets at startup or on demand.

What it buys: access control per secret, an audit log, automatic rotation for supported services such as databases, and encryption at rest with a managed key.

The authentication question — how the application proves it may read a secret — is solved by the platform's identity system. An instance with an attached role gets temporary rotating credentials automatically, so there is no bootstrap secret to leak. That is the elegant part: it removes the last hardcoded credential.

For a student project this is overkill. Understanding why it exists is worth having for interviews.

Practical rules

  • Never log secrets. A logged token ends up in log aggregation with far wider access than your database. Redact them explicitly.
  • Different secrets per environment. Development must never use production credentials — it removes the safety of a mistake being harmless.
  • Client-side means public. Anything in JavaScript sent to the browser, or in a mobile binary, is readable. Vite requires a VITE_ prefix for exposed variables precisely to make this a deliberate act.
  • Least privilege. A key that only reads should not be able to write. It limits the damage when — not if — one leaks.
  • Rotate periodically, not only after incidents, so rotation is a routine you have practised rather than something attempted for the first time under pressure.

See what recruiters look at on your GitHub — a committed .env is also a visible signal about how you work.

Frequently Asked Questions

I committed an API key and deleted it. Am I safe? No. It remains readable in the earlier commit, and git history is permanent. Rotate the key at the provider — that is the only action that actually removes the risk.
Should I commit my .env file? Never. Add it to .gitignore before creating it, and commit a .env.example with variable names and dummy values so others know what is required.
Are private repositories safe for secrets? Safer, not safe. Everyone with access has them, they exist in every clone and backup, and they persist after people leave the team.
Do I need a secrets manager for a student project? No. Environment variables are sufficient. Secrets managers add audit logging, per-secret access control and automatic rotation, which matter at organisational scale.
Can I put an API key in frontend JavaScript? Only if it is designed to be public, such as a publishable key. Anything shipped to the browser is readable, so secret keys must stay on the server.