Schema and types
Your committed .env.schema names every key your app needs. Values stay out of the repository, and penv gen writes typed code from the schema.
# @penv=acme/api-gateway
# The primary Postgres connection.
# @type=url
DATABASE_URL=
# @type=string(startsWith=sk_) @rotate=90d @hosts=api.stripe.com
STRIPE_SECRET_KEY=
# @type=enum(development,staging,production)
NODE_ENV=developmentYou commit .env.schema to your repository. It names every key your app needs and what a valid value looks like.
Schema and values
| File | Committed | Holds |
|---|---|---|
.env.schema | Yes | Key names, types, decorators, non-secret defaults |
.penv/config.toml | Yes | Settings, including [schema] version and [targets.<name>] |
.env, .env.local, .env.<env>, .env.<env>.local | No | Values, layered in that order (local and cloud modes) |
| Penv Cloud | Not a file | Values you store with us, in cloud mode |
penv check validates your values against the schema, and every other command refuses to run on a schema that does not parse.
Decorators
The decorator grammar is @env-spec: root decorators, item decorators, data types and functions.
| penv acts on | Purpose |
|---|---|
@penv=[<provider>:]<org>/<project> | Links the schema to a project; provider penv (Penv Cloud) is the default |
@currentEnv=$KEY | Names the key that selects the environment |
@hosts= | Seals the key to the hosts it may reach (sealed runs) |
@assert(expression, "message") | Checked by penv run (exit 3 before the child starts) and penv check |
@rotate=<span> | A rotation reminder that never blocks (rotation state) |
Values compute: ${KEY}, filters such as urlencode and base64, if, match, fallback, random(length=N), url(), json(), hash(), and penv() to read another environment, project or org. penv warns unknown_decorator on a decorator it does not act on.
penv check notes penv-only features that varlock will not load. The .env.schema format documents every addition, inference and diagnostic.
Rotation state
@rotate is a reminder; nothing replaces a value automatically. The due date is the current version's date plus the interval, so only a new value restarts the clock.
Units: y, m (calendar month), w, d, h, min, s, largest first, as in 90d or 1y6m. In local mode, penv records write times in .penv/config.toml and penv check names due keys.
| State | When | Console shows |
|---|---|---|
| Fresh | More than seven days before the due date | Rotates every 90d |
| Due soon | Inside the last seven days | Due in 3d |
| Overdue | Past the due date | Overdue 12d |
| Unrecognised | We cannot parse @rotate | Nothing |
A key you never wrote has no due date. We email overdue keys once a week to your workspace's security contact, naming addresses and never a value. Procedure: rotate a value.
Generated types
penv gen ts # src/env.ts: one typed env for server and client code, plus a Standard Schema validator
penv gen py # penv_env.py on the standard library, or pydantic when your project uses it
penv gen go # also rust, php, java, csharppenv gen writes code that ships with your repository, so nothing calls penv at runtime. A secret field prints [redacted] in every language.
| Behavior | Detail |
|---|---|
| Output location | Asked once, stored in [targets.<name>] in .penv/config.toml. --out <PATH> sets it non-interactively. An old .penv/targets/<name>/ folder migrates on the next gen |
Secrets in ts | Read by computed name, so no bundler inlines them into client code. The file masks sensitive values in console and Response bodies |
| Keys with a default | An unset or empty value takes the default, so the field's type never includes undefined |
| Build config | penv never edits tsconfig.json or package.json. It prints the import line, using an existing paths alias when one reaches the file |
Per-language detail: typed env for a language.
Next: local mode and cloud mode.
Environments and precedence
penv reads the environment named by --env, then PENV_ENV, then the @currentEnv key, then development. We decide whether your identity may read it.
Local mode and cloud mode
Your schema header and a credential decide the mode. Local files layer over cloud values, and a sealed local copy lets warm runs skip the round trip to us.