Docs
Guidesince 1.0.0-alpha.3

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

GuessHow
TypeFrom the shape of the value, written as a @type= decorator.
RequiredAn empty value is required. A value on the line becomes the default and makes the key optional.
SensitiveSensitive 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:

.env.schema
# @schema=1

# @type=url
DATABASE_URL=

# @type=string
STRIPE_SECRET_KEY=

# @type=port @sensitive=false
PORT=3000

# @type=boolean @sensitive=false
DEBUG=true

What 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.

.env.schema
# 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=development

penv 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.

Set up your workspace in the console