A `.env` file looks like the simplest possible format: `KEY=value`, one per line. It has more edge cases than that suggests, and one property that causes a specific, recurring bug.
Everything is a string
Environment variables have exactly one type. This line:
DEBUG=falsesets `DEBUG` to the string `"false"`. And a non-empty string is truthy in JavaScript, Python, Ruby, PHP and most other languages. So this does not do what it appears to:
if (process.env.DEBUG) {
enableVerboseLogging() // runs even when DEBUG=false
}The only way to turn it off is to remove the variable entirely or set it empty. Coerce explicitly instead:
const DEBUG = process.env.DEBUG === 'true'
const PORT = Number(process.env.PORT) || 3000The same applies to numbers. `PORT=3000` gives you `"3000"`, and `"3000" + 1` is `"30001"`.
The quoting rules
Three forms, three behaviours:
| Line | Resulting value | Notes |
|---|---|---|
| `KEY=hello world` | `hello world` | Unquoted; works but fragile |
| `KEY="hello world"` | `hello world` | Quotes removed |
| `KEY='hello world'` | `hello world` | Quotes removed, taken literally |
| `KEY="line1\nline2"` | two lines | `\n` expanded in double quotes |
| `KEY='line1\nline2'` | literal `\n` | not expanded in single quotes |
| `KEY=value # comment` | `value` | inline comment stripped |
| `KEY="value # hash"` | `value # hash` | hash kept inside quotes |
The single-versus-double distinction matters when a value contains `
Comments and export
Lines beginning with `#` are ignored. A leading `export ` is stripped, which exists so the file can also be `source`d by a shell:
# Database configuration
export DATABASE_URL=postgres://localhost:5432/app
API_KEY='sk-live-1234567890'Both variable lines are equivalent to a dotenv loader.
Spaces around the equals sign
KEY = value # wrong in most loaders
KEY=value # rightShell syntax does not allow spaces around `=`, and most dotenv implementations follow suit. The spaces end up in the key, the value, or both — and the resulting bug looks like the variable simply is not set.
Converting to and from JSON
Deployment platforms and CI systems usually want JSON, while local development wants a `.env`. The ENV to JSON converter goes both ways, handling comments, export prefixes and quoting.
It runs in your browser, which matters here — a `.env` file is by definition the file with your secrets in it. Even so, prefer exporting directly from a secrets manager where you can, and clear the field afterwards.
Common mistakes
- Committing the file. `.env` belongs in `.gitignore`. If you have already committed secrets, rotate them — deleting the file in a later commit leaves it in history.
- Assuming nesting works. Dotenv is flat. There is no `DATABASE.HOST` structure; the convention is `DATABASE_HOST`. To go from nested config, flatten it first with the JSON flattener.
- Multi-line values without quotes. A private key spanning several lines needs double quotes with `\n` escapes, or only the first line is read.
- Expecting load order to matter. Variables are a flat namespace with no ordering guarantees. One line cannot reliably depend on an earlier one unless your loader explicitly supports interpolation.
- Relying on `.env` in production. It is a development convenience. Production secrets belong in your platform secret store, where they are encrypted and access-controlled.
Frequently asked questions
Should .env be committed?
Never. Commit a `.env.example` with the keys and placeholder values so others know what to set.
Why is my variable undefined?
Usual causes: the loader runs after the code reading the variable, spaces around the equals sign, the file is in the wrong directory, or the process was not restarted after editing.
Does the order of variables matter?
No, unless your loader supports interpolation — and even then, relying on it is fragile.
Can values contain the equals sign?
Yes. Only the first `=` splits the line, so `KEY=a=b` gives `a=b`. This matters for base64 values, which often end in `=`.
What is the difference between .env and .env.local?
Convention, not standard. Most frameworks load `.env` first, then `.env.local` to override it, and gitignore only the latter. Check your framework's documentation.
Related reading
For nested configuration that needs to become flat keys, the JSON flattener does the conversion. If your config includes API tokens, the JWT decoder inspects claims locally so you can check an expiry without pasting a token into a third-party site.