slopware 101 · step 11 of 35
Who else can see your API key?
by Damla Su Bilge · 4 min read
Saving a project's history makes it easier to recover earlier work. It also preserves anything sensitive that went into those earlier files. An API key removed from today's code may still be sitting in yesterday's commit.
A secret grants access just by being known: an API key, a database password, a token or a signing key. Programs need a way to read these values. We need to give them that access without handing the same access to everyone who can open the page or repository.
The browser is the wrong place, because everything the browser downloads belongs to whoever holds it. The public keys that managed services hand you for the frontend are an exception by design. They identify a project and work within its configured public permissions, so they are not secrets. Privileged keys stay on the service side, injected through environment settings or a secret store rather than shipped to the browser.
Keys in source code
Models sometimes write a key straight into the code to make it run.
const client = new VisionClient('sk_live_5f2a...');
A key in the source goes wherever the source goes: into git history, into every clone, into any screenshot of the editor. Changing the key becomes a code change and a new deploy.
Environment variables
The environment is the set of named values a process is handed when it starts. An environment variable is a value the program reads from there instead of from its source code.
const client = new VisionClient(process.env.VISION_API_KEY);
The same code can now start with a different value on each machine. On macOS or Linux you can try it directly with VISION_API_KEY=test node server.js.
.env is just a file
.env is a text file of NAME=value lines. The operating system does not read it. Your framework, or a library like dotenv, loads it into the environment when the program starts. That has two consequences. If the tool is not loading it, because the file is in another folder or the production host never sees it, the variable is simply missing. And .env protects nothing by itself. It is plain text, readable by processes with permission to access that file, and git will commit it like any other file.
Adding .env to .gitignore tells git not to track it, but only if git is not already tracking it. If it was committed once, git rm --cached .env stops tracking it without deleting your local copy. Every earlier commit still contains it, and anyone who cloned the repository already has it. A private repository narrows the audience. It is not a vault: everyone with access, every connected service and any future leak can read it.
To search the history, use a recognisable part of the key.
git log -p -S 'sk_live_5f2a' --all
A leaked key is rotated, not hidden
If a key has ever been committed, assume it is out. Rotating means issuing a new key in the provider's dashboard, putting it where your programs read it, deploying and checking that everything still works, and then revoking the old one. The leaked key stays valid until it is revoked. Rewriting git history to remove it afterwards is tidying, not a fix.
Dev and prod
Dev is the copy running on your own machine while you work. Prod is the copy your users open. They need different values: separate API keys, and above all separate databases. Your dev database is where migrations get tried, test rows pile up, and a model's enthusiastic delete lands. If dev and prod share one database, every experiment runs on real users' data. A separate test database keeps those experiments away from live records.
Prod values do not live in a file in the repository. They go into your host's environment settings, or into your CI's secrets settings, where the running program receives them at startup.
Variables that reach the browser
Environment variables split once more at build time. The build runs before the app ships and produces the files the browser downloads. Frameworks let some variables be written into those files on purpose: in Next.js the ones that start with NEXT_PUBLIC_, in Vite the ones that start with VITE_. Any value with that prefix is copied into the JavaScript bundle as plain text, and changing it later needs a new build. Those prefixes are framework conventions, not a security boundary by themselves. Build configuration, plugins or code that returns a value can expose an unprefixed variable too. Inspect the generated output and the path that uses the value.
You can watch it happen. Use two fake marker strings in .env, never working credentials: one with the documented public prefix and one without. Follow your framework's access syntax and run a production build. Open the built site, then search all loaded files in DevTools with Cmd+Option+F on a Mac or Ctrl+Shift+F elsewhere. Check which marker reached the client bundle. If a private marker appears, trace how the build exposed it before introducing a real secret.
When a variable comes out undefined in browser code, the quick fix a model often suggests is adding the public prefix. For a secret, that fix publishes it. The right move is to call your own server and let it use the key.
In your repository
Search your repository's history for your keys. If one turns up, rotate it before you fix the file, because the copy in the history is already out. Make sure .env is in .gitignore. Then list every variable with a public prefix and confirm that each one is something you would be comfortable printing on your landing page.


