How-to › Authenticate and authorize calls

How to keep test and live API keys from crossing environments#

Put the environment in the key itself and check it at process start, so a staging deployment holding a production credential refuses to run.

Audience
platform team
Level
beginner
Topic
Send API keys and bearer tokens
Languages
TypeScript and JavaScript
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
A prefix the code checksYou issue the keys, or the provider already prefixes themNothing catches a key with the right prefix used in the wrong account, so it is one layer of twoThe provider issues opaque keys you cannot classify
Separate credential storesDeployments already read secrets from a store rather than from filesA store to run, and a path that has to be right in every deployment manifestA single small service reading environment variables
Scanning before commitKeys keep arriving in commits and you want them stopped locallyA hook every contributor has to install, and it sees only what git seesThe leak path is a log or an error tracker rather than a commit
Provider-side scanningThe code is on a forge that supports it and the provider participatesIt reports after the push, so it shortens exposure rather than preventing itThe 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.

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.

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 22 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.