A staging deployment picks up a production key because one environment variable was copied between two dashboards. Nothing fails. Staging talks to the live API for a week, and the first sign is a customer asking about test invoices in their real account. The credential worked, which is exactly the problem.
What you get
You will end up with keys that carry their own environment, and a guard that refuses to start a process holding the wrong one. This is for you if the same variable name holds a different secret in each deployment.
Short answer
Give every key a prefix that names its environment, then assert at process start that the prefix matches the environment the process believes it is in. A mismatch throws before any call goes out. The same prefix lets a secret scanner recognize the key in a repository, and lets a log line identify a key without printing it.
You will need
Node 22 or later, and control over how your keys are issued, or an API that already prefixes them. Stripe’s key format is the widely copied example, and the pattern below is the same idea applied to keys you issue yourself.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A prefix the code checks | You issue the keys, or the provider already prefixes them | Nothing catches a key with the right prefix used in the wrong account, so it is one layer of two | The provider issues opaque keys you cannot classify |
| Separate credential stores | Deployments already read secrets from a store rather than from files | A store to run, and a path that has to be right in every deployment manifest | A single small service reading environment variables |
| Scanning before commit | Keys keep arriving in commits and you want them stopped locally | A hook every contributor has to install, and it sees only what git sees | The leak path is a log or an error tracker rather than a commit |
| Provider-side scanning | The code is on a forge that supports it and the provider participates | It reports after the push, so it shortens exposure rather than preventing it | The repository is not on a participating forge |
These are layers rather than alternatives. The prefix check is the one that runs on every start of every process, which is why it goes first. Scanning catches the key that reached a repository, and a credential store stops the copy-and-paste that put it there. A team running all three still needs the prefix check, because the store hands out whatever it was told to hold.
Put the environment in the key
The prefix is a contract between the issuer and every process that reads a key.
const PREFIXES = { test: 'sk_test_', live: 'sk_live_' }
export function environmentOf(key) {
for (const [env, prefix] of Object.entries(PREFIXES)) {
if (key.startsWith(prefix)) return env
}
return null
}
export function assertKeyMatchesEnvironment(key, environment) {
const keyEnv = environmentOf(key)
if (keyEnv === null) {
throw new Error(`key does not carry an environment prefix, expected one of ${Object.values(PREFIXES).join(', ')}`)
}
if (keyEnv !== environment) {
throw new Error(`refusing to use a ${keyEnv} key while running in ${environment}`)
}
return keyEnv
}
An unrecognized key is refused rather than allowed. That decision is the one that gets softened under deadline, and softening it removes the guarantee. A key with no prefix could belong to either environment. Treating it as the safe one is a guess dressed as a policy.
The prefix has to be applied where the key is minted, which means the issuing endpoint rather than the client. Generate the secret with a cryptographic random source and prepend the prefix for the environment the request came from. Store a hash of the whole string rather than the string itself. The prefix then travels with the key through every environment variable, deployment manifest and support ticket it passes through, and no later step has to remember what it was for.
Call the assertion where the configuration is read, not where the first request is made. A process that starts, passes a health check and then fails on its first customer request is harder to notice than one that refuses to start at all.
Make the key safe to log
A key you cannot mention is a key nobody can debug. The prefix gives you a way to say which credential a process is holding without printing the secret.
/** Redacts a key for a log line: the prefix identifies it, the secret does not leak. */
export function redact(key) {
const env = environmentOf(key)
return env === null ? 'unrecognized' : `${PREFIXES[env]}...${key.slice(-4)}`
}
Four trailing characters are enough to tell two keys apart in a log and far too few to use. Take them from the end rather than the start, because the start is the prefix and every key in an environment shares it. Print that on startup beside the environment name, and the mismatch becomes visible in the deployment log even where the guard is not running.
Check it worked
Run the three cases a deployment can actually be in.
node demo.mjs
test sk_test_...p7dc accepted
test sk_live_...p7dc refused: refusing to use a live key while running in test
production unrecognized refused: key does not carry an environment prefix, expected one of sk_test_, sk_live_
The second line is the failure this page exists for, and the third is the one that would otherwise be waved through. Both refuse before any request leaves the process.
node --test guard.test.mjs
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 107.58153
When it goes wrong
The guard passes and the key is still wrong. A prefix names an environment, and it says nothing about which account inside that environment the key belongs to. Two live keys from two customer accounts both pass. Where that distinction matters, have the process fetch its own identity from the API at startup and compare it with the account it expects.
The second failure is a prefix that drifts. A provider that adds a third environment, or renames one, breaks a hardcoded map, and the failure mode is the safe one only if unrecognized keys are refused. Read the prefix list from the same configuration that issues the keys, and a new environment arrives as data rather than as a deploy.
When not to do this
Do not use a prefix as the only separation between environments. It catches the key in the wrong process, and it does nothing about a test process pointed at a production database. Separate accounts, separate stores and separate networks are what actually keep the two apart.
Do not put the environment in the key if your provider issues keys you cannot change. Deriving a prefix from a key by hashing it, or by remembering the first eight characters, gives a check that fails the first time a key is rotated.
Do not log the redacted form at debug level and the full key anywhere else. One code path that prints the whole secret undoes the redaction everywhere, and the place it usually appears is an error object serialized into a tracker.
Do not treat a passing guard as permission to keep production credentials in developer environments. The guard stops a process from using the wrong key. Nothing stops a person from reading it.
Related how-tos
Last verified
Verified 2026-09-06 against Node 22.22.2. Both output blocks are what the preceding command printed. The key values are fictitious and follow the documented Stripe prefix format.