CONSTANT_CASE for Environment Variables: A 2026 Guide
CONSTANT_CASE is the only naming convention environment variables reliably survive. Here is what POSIX requires, what breaks, and how to convert safely.
CONSTANT_CASE for Environment Variables
Every .env file you have ever opened looks the same: DATABASE_URL, STRIPE_SECRET_KEY, LOG_LEVEL. That shape has a name — CONSTANT_CASE — and using it for environment variables is not a style preference you can override in a linter config. The shell, POSIX, and every container orchestrator treat the uppercase-and-underscore form as the only safe one. Here is why, and where deviating actually breaks.
TL;DR
- POSIX reserves lowercase environment variable names for applications, uppercase for the system.
- Names allow only letters, digits, and underscore — and must not start with a digit.
- Linux and macOS are case-sensitive; Windows is not. That gap causes real bugs.
- Kubernetes silently skips ConfigMap keys containing dots or hyphens.
- Prefix names by service, and treat build-tool prefixes like
VITE_as a security boundary.
What CONSTANT_CASE actually is
CONSTANT_CASE writes every word in uppercase and joins them with a single underscore: MAX_RETRY_COUNT. Nothing else is allowed inside the token — no spaces, no hyphens, no dots, no camel humps.
The other four names for the same thing
The convention travels under several names depending on which community you learned it in, and they all mean exactly the same shape:
| Name | Where you hear it |
|---|---|
| CONSTANT_CASE | Tooling libraries, case-conversion APIs |
| SCREAMING_SNAKE_CASE | JavaScript and Python style guides |
| UPPER_SNAKE_CASE | Linters, config schemas |
| MACRO_CASE | C and C++, from #define convention |
There is no technical difference between them. If a library exposes a toConstantCase() helper and a blog post tells you to use SCREAMING_SNAKE_CASE, you are being told to do the same thing.
Why C macros set the precedent
The convention is older than the web. In C, the preprocessor substitutes macros textually before the compiler sees the file, which means a macro can silently replace something you thought was a variable. Uppercase was adopted as a visual warning: this token is not a normal identifier, it gets rewritten before compilation. Environment variables inherited the same logic — the name refers to something injected from outside the program, not declared inside it.
Where CONSTANT_CASE is not the right answer
Inside application code, CONSTANT_CASE has narrowed considerably. A const in modern JavaScript is not a compile-time constant, it is a binding that cannot be reassigned, so const userName = "ana" is correctly camelCase. Reserve the uppercase form for true module-level constants and for anything crossing the process boundary: env vars, feature flags read from config, and enum-like frozen values. Everything else follows the language's normal identifier convention — see the camelCase vs snake_case vs kebab-case comparison for the full decision table.
Why are environment variables uppercase?
This one has a written answer, in a standard, and it is more specific than "convention".
What POSIX actually requires
The Open Group Base Specifications Issue 8 (IEEE Std 1003.1-2024) states in its Environment Variables chapter that environment variable names used by the standard shell utilities consist solely of uppercase letters, digits, and the underscore, drawn from the portable character set, and do not begin with a digit. That is the rule that produced PATH, HOME, TZ, TMPDIR, and the whole LC_* family.
The same chapter adds the part people usually miss: the namespace of environment variable names containing lowercase letters is reserved for applications. Applications can define any variable from that namespace without changing how the standard utilities behave.
So the case split is a namespace boundary, not decoration. Uppercase means "this may collide with something the system already defines". Lowercase means "this is mine alone". Almost everyone uses uppercase anyway — which is why the POSIX spec also publishes a list of roughly seventy names it is unwise to conflict with, including CC, EDITOR, PAGER, RANDOM, SHELL, and USER. If your service reads a bare USER or HOME variable as its own config, you are reading whatever the login shell set.
Uppercase and lowercase are never folded together
POSIX is explicit that uppercase and lowercase letters retain their unique identities and are not folded. On any Unix-like system, PATH, Path, and path are three unrelated variables. This is why the following does exactly what it says and nothing more:
export API_KEY=live_123
echo "$Api_Key" # prints an empty line
echo "$API_KEY" # prints live_123
Why .env files inherited the rule
A .env file is not a standard — it is a convention that exists because the Twelve-Factor App methodology pushed config out of code and into the environment, and developers needed a way to populate that environment locally. Because the file's job is to become a set of real environment variables, it inherits every constraint the environment has. A dotenv parser that accepts my.api.key=x is doing you no favour: the moment that value reaches a container runtime, the name is invalid.
What characters are allowed in an environment variable name
The allowed set is small, and every tool in the chain enforces a slightly different subset of it. The intersection is what you should actually target.
| Character | Allowed | Note |
|---|---|---|
A–Z |
Yes | The portable, expected form |
a–z |
Yes | Reserved for applications by POSIX |
0–9 |
Yes | Never as the first character |
_ |
Yes | The only word separator |
- . = |
No | = is the name/value delimiter itself |
Can an environment variable name start with a number?
No — not portably. POSIX requires that names used by the standard utilities do not begin with a digit, and it attaches a note recommending against digit-initial names anywhere, because other applications have difficulty with them. In practice your shell will reject the assignment outright:
$ 2FA_SECRET=abc
bash: 2FA_SECRET=abc: command not found
The shell parsed 2FA_SECRET=abc as a command name rather than an assignment, because an assignment must start with a valid name. Rename it TWO_FACTOR_SECRET and move on.
Why dots and hyphens break in Kubernetes
This is the failure mode that costs people an afternoon. ConfigMap keys are permissive — app.database.host is a perfectly legal key. Environment variable names are not. When you mount a ConfigMap with envFrom, Kubernetes converts each key into a variable name, and per the Configure a Pod to Use a ConfigMap guide, keys that are not valid environment variable names are skipped, the Pod is still allowed to start, and the skipped names are recorded in an InvalidVariableNames event.
Nothing crashes. Your app just reads undefined and falls back to a default. The diagnostic is in kubectl get events, not in your logs:
kubectl get events \
--field-selector reason=InvalidVariableNames
If you keep ConfigMaps and Secrets in YAML, it is worth round-tripping them through a formatter before applying — a misindented data: block fails differently from an invalid key, and telling the two apart quickly matters. iKit's JSON ↔ YAML converter does that in the browser without sending manifests anywhere.
Windows is case-insensitive, and that bites cross-platform teams
Node.js documents this directly: on Windows operating systems, environment variables are case-insensitive. So process.env.apiKey, process.env.APIKEY, and process.env.API_KEY… well, the first two resolve to the same entry on Windows and to nothing on Linux. See the process.env documentation for the exact wording and the assignment coercion rules that go with it.
The practical consequence: a developer on Windows writes process.env.Database_Url, it works locally, and it returns undefined the first time CI builds a Linux image. Writing every name in CONSTANT_CASE on both sides makes the platform difference unobservable, which is the entire point of a convention.
How to name environment variables in a real project
The character rules tell you what is legal. They do not tell you what is maintainable. Three rules cover most of it.
Prefix by service, not by nothing
A bare PORT or TIMEOUT is fine for a single process and a liability the moment two services share a compose file or a Kubernetes namespace. Prefix with a short, stable service token:
BILLING_DB_URL,BILLING_DB_POOL_SIZESEARCH_INDEX_HOST,SEARCH_INDEX_TIMEOUT_MSNOTIFY_SMTP_HOST,NOTIFY_SMTP_PORT
The prefix also gives you a grep-able boundary and a natural way to hand a subset of the environment to a sidecar. Put the unit in the name (_MS, _BYTES, _SECONDS) when the value is a number — it is the cheapest documentation you will ever write.
Build-tool prefixes are a security boundary, not a style
Modern front-end tooling uses the prefix to decide what gets inlined into the bundle a browser downloads. Vite's Env Variables and Modes documentation is blunt about it: only variables prefixed with VITE_ are exposed in client source code. Given VITE_SOME_KEY=123 and DB_PASSWORD=foobar, import.meta.env.VITE_SOME_KEY is "123" in the browser and import.meta.env.DB_PASSWORD is undefined. Next.js uses NEXT_PUBLIC_ for the same purpose.
Read that in reverse and it becomes a rule: adding a prefix is a decision to publish the value to every visitor. VITE_STRIPE_PUBLISHABLE_KEY is fine. VITE_STRIPE_SECRET_KEY is a leak with a deploy attached.
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_SENTRY_DSN=https://[email protected]/1
STRIPE_SECRET_KEY=sk_live_xxx # server only
DATABASE_URL=postgres://... # server only
When you need to generate one of those server-only values, generate it with a CSPRNG rather than typing something memorable — iKit's password and key generator produces URL-safe random strings entirely in the browser tab, so the secret never crosses a network.
Everything is a string, so name for the parse
The environment has exactly one type. DEBUG=false arrives in your process as the five-character string "false", which is truthy in JavaScript, Python, and Ruby alike. This is the single most common env-var bug there is.
// wrong: "false" is a non-empty string
const debug = process.env.DEBUG;
// right: parse at the boundary, once
const debug = process.env.DEBUG === "true";
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
if (Number.isNaN(port)) throw new Error("PORT is not a number");
Name booleans so the expected values are obvious — ENABLE_METRICS, DISABLE_CACHE, IS_PRODUCTION — and validate the whole environment once at startup rather than at each read site.
How to convert camelCase to CONSTANT_CASE
You will do this every time you generate a config schema from application code, or import a settings object into a deployment manifest.
The naive regex breaks on acronyms
The obvious approach — insert an underscore before each capital, then uppercase everything — collapses the moment an acronym appears:
// naive
"XMLHttpRequest"
.replace(/([A-Z])/g, "_$1")
.toUpperCase();
// "_X_M_L_HTTP_REQUEST" ❌
Every letter of XML is a capital, so every letter gets its own boundary. The same input should become XML_HTTP_REQUEST.
The two-pass version that handles them
You need two separate boundary rules: one that splits an acronym run from a following capitalised word, and one that splits a lowercase letter or digit from a following capital.
function toConstantCase(input) {
return input
// ACRONYM|Word → ACRONYM_Word
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
// word|Word → word_Word
.replace(/([a-z\d])([A-Z])/g, "$1_$2")
.replace(/[\s\-.]+/g, "_")
.toUpperCase();
}
toConstantCase("apiBaseUrl"); // API_BASE_URL
toConstantCase("XMLHttpRequest"); // XML_HTTP_REQUEST
toConstantCase("s3BucketName"); // S3_BUCKET_NAME
toConstantCase("oauth2ClientId"); // OAUTH2_CLIENT_ID
The third replace is the one that makes it safe for environment use: it folds spaces, hyphens, and dots into underscores, so app.database.host and feature-flag-name both come out legal. The acronym edge cases are the same ones that show up converting to any other convention — the acronym rules for case conversion cover why S3Bucket and IOError need different handling than XMLHttpRequest.
When not to write the regex at all
For a one-off list of keys — pulling a settings object out of a codebase, or renaming a batch of ConfigMap entries — maintaining the regex is not worth it. Paste the identifiers into iKit's Case Converter, pick CONSTANT_CASE, and copy the result back. It runs entirely in the browser tab, which matters when the list you are converting happens to contain the names of your production secrets.
References
- The Open Group Base Specifications Issue 8 — Chapter 8, Environment Variables — the normative character set, the digit-initial restriction, the reserved-lowercase namespace, and the list of names not to conflict with.
- The Twelve-Factor App — III. Config — the case for storing config in the environment rather than in committed files.
- Configure a Pod to Use a ConfigMap —
envFrombehaviour when a ConfigMap key is not a valid environment variable name, and theInvalidVariableNamesevent. - Node.js Documentation —
process.env— case-insensitivity on Windows and the string coercion applied to assigned values. - Vite — Env Variables and Modes — the
VITE_prefix rule and which variables are exposed to client bundles.
Related on iKit
- The full comparison of camelCase, snake_case, and kebab-case — where CONSTANT_CASE sits among the other four conventions, and the per-language decision table this article assumes.
- Converting camelCase to snake_case without breaking your code — the same two-pass boundary regex, applied to the lowercase underscore form.
- XMLHttpRequest to xml_http_request: the acronym rule — a deeper look at why acronym runs need their own replace pass before any case conversion.
- kebab-case in CSS, URLs, and CLI flags — the mirror-image argument: why hyphens win in declarative formats and are illegal in environment names.
- PascalCase and the acronym rules that trip up class names — how the same
HTTPServervsHttpServerquestion plays out one layer up, in type names. - REST API naming: snake_case backend, camelCase frontend — the other place a case boundary becomes a translation layer you have to maintain.
Related posts
How Many Words of Lorem Ipsum Does a Mockup Need? (2026)
How many words of lorem ipsum a mockup needs depends on the container, not habit. Here is the formula, per-block targets, and why 30 words is everywhere.
REST API Naming: snake_case Backend, camelCase Frontend (2026)
REST API naming forces a choice: snake_case on the backend, camelCase in the client. Here is where to convert, what the round trip loses, and how to decide.
Why Designers Still Use Lorem Ipsum in 2026 (And When Not)
Lorem ipsum survived sixty years of design tooling for one reason: it fails loudly. Here is what filler text is genuinely good at, and where it costs you.