serez-dotenv
A project-level .env loader for Serez Code. It reads .env, .env.local and .env.<mode>, merges them by priority, and resolves variables read-only — it never mutates the real process environment.
Install
sz install serez-dotenvserez-dotenv declares the Env permission itself (it only uses it for the fallback to the system environment), so apps that install it do not need to add Env to their own serez.json. Reading the .env files uses File, which needs no permission.
Quick start
Construct a Process with the mode and read variables through process.env. The .env files are read from the current working directory (the project root).
import "serez-dotenv"
let process = new Process("dev") // mode: dev | qa | local
let host = process.env.get("HOST") // resolved value, or null if undefined
if (host == null) {
out "HOST is not set"
} else {
out "HOST = " + host
}Modes
The mode passed to new Process(mode) decides which layers are loaded:
| Mode | Layers loaded |
|---|---|
dev | .env + .env.local + .env.dev |
qa | .env + .env.local + .env.qa |
local | .env + .env.local |
Layered resolution
When you read a key, the most specific layer that defines it wins. If none of the .env layers define it, the lookup falls back to the system environment as a last resort. From lowest to highest priority:
System < .env < .env.local < .env.<mode> (dev | qa).env— base layer, always loaded if present..env.local— the developer's personal overrides, always loaded. It always loses to.env.dev/.env.qa— by design, so personal tweaks never harden the deployed config..env.<mode>— top layer, only indevorqamode. Inlocalmode there is no dev/qa layer.
Example — in dev mode, when every layer defines HOST, .env.dev wins:
System HOST ✗
.env HOST ✗
.env.local HOST ✗
.env.dev HOST ✓ ← winsAPI
| Call | Returns | Description |
|---|---|---|
new Process(mode) | Process | Loads the current directory's layers for the given mode (dev | qa | local). |
process.env.get(key) | string? | Value resolved across layers, falling back to System; null if undefined everywhere. |
process.env.has(key) | bool | true if the key is defined in some .env layer (ignores System). |
process.mode | string | The mode the Process was constructed with. |
loadEnv(dir, mode) | EnvResolver | Loads the layers from an arbitrary directory — handy for tests or reading another folder's .env. |
process.env.get(...) returns null (it does not throw) when the variable exists in no layer and not in the system environment.
The .env file format
# Comments start with '#' (whole line only)
HOST=localhost
PORT=8080
DB_URL=postgres://user:pass@host/db
QUOTED="with spaces" # surrounding quotes are stripped
SINGLE='also single quotes'
export TOKEN=abc123 # the 'export' prefix is ignored
EMPTY= # empty value allowed- One variable per line:
KEY=VALUE. - Blank lines and lines starting with
#are ignored. - Whitespace around the key and value is trimmed.
- Surrounding
"…"or'…'quotes are removed. - An optional
exportprefix is discarded. - If a key repeats within the same file, the last one wins.
#is only a comment at the start of a line — there are no inline comments.
A complete example
.env
HOST=base-host
PORT=3000
DB_URL=postgres://localhost/app.env.dev
HOST=dev-host
DEBUG=trueapp.sz
import "serez-dotenv"
let process = new Process("dev")
let host = process.env.get("HOST") // "dev-host" (.env.dev overrides .env)
let port = process.env.get("PORT") // "3000" (only in .env)
let debug = process.env.get("DEBUG") // "true" (only in .env.dev)
let user = process.env.get("USER") // from the system, or null if unsetDesign notes
- Read-only: it never calls
Env.set— the process environment is never polluted. - No dependencies: pure
.szon top of the core (FileandEnvnamespaces). - The
.envfiles are read from the current working directory (the project root); useloadEnv(dir, mode)to read another folder. - Every value resolves to a
string(ornull) — parse numbers and booleans yourself.