From a .env file
What penv init reads, and what it refuses to copy into the schema.
penv init turns the file you already have into the one file you commit. Nothing is sent
anywhere, and the file you started with stays where it is.
What init reads
The .env in the directory you run it in. penv reads it and writes .env.schema beside it, with
a type worked out for every key.
If .env.schema already exists, init stops and tells you to run penv check or
penv init --force. Nothing is overwritten by accident.
What it works out for each key
| Guess | How |
|---|---|
| Type | From the shape of the value, written as a @type= decorator. |
| Required | An empty value is required. A value on the line becomes the default and makes the key optional. |
| Sensitive | Sensitive unless the key carries a bundler prefix such as NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_, NUXT_PUBLIC_ or REACT_APP_. |
The result is a file a person can read:
# @schema=1
# @type=url
DATABASE_URL=
# @type=string
STRIPE_SECRET_KEY=
# @type=port @sensitive=false
PORT=3000
# @type=boolean @sensitive=false
DEBUG=trueWhat it never copies
No value that could be a secret is ever copied into the schema.
A value becomes a default only when the key carries a bundler prefix, or when the value is dull
enough to be certain about: a boolean, an integer, a lowercase word of letters, a short lowercase
slug such as us-east-1, gpt-4o or api.internal, or a localhost URL with no user information
and no query string.
On top of that, a key named for what it holds keeps its value out however dull the value reads and
whatever prefix it carries. Those words are AUTH, KEY, SECRET, TOKEN, PASSWORD,
PASSWD, PASSPHRASE, PASS, PWD, PW, CREDENTIAL, CRED, DSN, SALT, SEED and
SIGNATURE, each matching the word itself or its plural. So NEXT_PUBLIC_SUPABASE_ANON_KEY keeps
its value out of the schema, while still being marked public because a bundler prefix reaches the
browser either way.
Correct a guess
Edit .env.schema by hand. Decorators are # comment lines directly above a key, order never
matters, and the vocabulary follows @env-spec, so a varlock user reads the
file on sight.
# The gateway the API talks to. A comment line without @ is the key's description.
# @type=string(startsWith=sk_) @rotate=90d
STRIPE_SECRET_KEY=
# @type=enum(development,staging,production)
NODE_ENV=developmentpenv check reads the file back and names what it refuses. An unknown decorator is an error. A key
that carries both a bundler prefix and @sensitive is an error, because those two cannot both be
true.
Lines penv cannot write back
penv reads and writes the dotenv subset that Node's util.parseEnv and dotenv both read back.
Inside double quotes, \n is an escape and nothing else is, which is how a multi-line value such
as a PEM key sits on one line.
The reader is more forgiving than the writer: it decodes \r, \t, \" and \\ so a file
another tool wrote is read rather than mangled, and warns once per value, naming the line and the
column and never the character. init counts those lines and penv check names them.
The undo
.env stays on disk until penv push deletes it, and penv pull writes it back from the cloud
whenever you want it. The file is a view of the values, so losing it costs nothing.
In local mode nothing is ever deleted. init writes a schema and adds .env to your .gitignore,
and your own file stays where it is. To back all of it out, delete .env.schema and the
.gitignore line penv added.