A better place for your .env file
Per-environment variables, encrypted at rest and injected without a plaintext file. Still need a .env? You can have one.
What a variable actually carries
An environment is a set of variables inside a project. Each carries a type, server or client visibility, and an encrypted value the server cannot read.
Full definition
An Envless environment is a named set of variables inside a project, and each variable carries more than a string: a type of string, number or boolean that the runtime coerces on read, a visibility flag of server or client that decides whether a bundler may inline it, and an encrypted value the server cannot read. You import an existing .env once, and from then on you either inject the values into a process directly, generate TypeScript declarations from the schema, or write a real .env file back to disk on demand.
Sync to .env files
File naming, the generated header, idempotent writes, and the gitignore guard.
Run a process with variables injected
Signal forwarding, exit codes, environment merge order, and the names that are never injected.
Typed variables
Generating declarations, how coercion works, and overriding values inside tests.
Server vs client variables
How the visibility flag is enforced through bundler plugins, an ESLint rule and runtime checks.
Import an existing .env
What push accepts, what it skips, and how --dry-run and --prune behave.
Free .env tools
Validate, diff, convert and strip .env files in your browser before any of them reach a repository.
string, number, boolean
3 typesStored on the row with string as the default. The runtime coerces on read, so env.PORT is a number and env.DEBUG is a boolean without a parse call at every use site.
server or client
2 visibilitiesOne flag, defaulting to server. The client proxy only passes variables flagged client, and reading a server-only name from it throws at runtime rather than quietly returning undefined.
Refused injection outright
26 namesNames like PATH, NODE_OPTIONS, LD_PRELOAD, RUBYOPT and PYTHONPATH change how a process or its children execute code, so storing one would turn write access to a variable into code execution. They are decrypted and readable, but never written into a process environment, and each one skipped is named in a warning.
File mode on every sync
0600envless sync writes one .env.<slug> per environment, name-sorted, behind a generated header, atomically via a temp file plus rename. Identical output is skipped rather than rewritten, so a repeated sync never retriggers a dev-server reload.
On .env import
6 diagnosticsThe parser reports missing_equals, empty_key, invalid_key, unterminated_quote, trailing_characters and duplicate_key with line numbers, and every parsed row is previewed with an add, replace or skip action before anything is saved.
On a concurrent edit
409Bulk edits carry a baseline of every live variable. The server takes an advisory lock on the environment, recomputes drift, and names the exact variables that changed under you instead of silently overwriting a colleague.
Environment variables FAQ
No. The goal is that no plaintext .env is the source of truth or gets copied between people, not that the file format is banned. envless sync writes real .env.<slug> files whenever a tool needs one, and envless run skips the file entirely when nothing does. Most teams end up using run for application processes and sync for the one or two tools that only read files.
It is stored and it is readable, but it is never written into a process environment. Fifteen names change how a process or its children execute code, so injecting one would turn write access to a variable into code execution. Both the CLI and the runtime refuse them and print a warning naming each one skipped, and the value is still available through the typed proxy if you genuinely need to read it.
No, and deliberately so: editing a repository file on your behalf is a surprising thing for a secrets tool to do. Sync shells out to git check-ignore and warns you when the target is tracked, and --require-gitignore turns that warning into a hard refusal that writes nothing. The .gitignore entry stays your decision.
Not with sync, which writes a separate .env.<slug> per environment. Merging is a run and build behaviour: pass --env staging,production and the child process gets both merged left to right, with the later environment winning on a name collision. The two are deliberately different, because a merged file on disk has no record of which environment a value came from.
Only when you import a .env through the dashboard, where true and false become boolean, anything finite becomes number, and the rest stay string. envless push and the API do not infer; they default to string. You can always set the type explicitly afterwards, and the type is what the runtime coerces against on read.
Today it is a flag stored on the variable that the environment compare view reads, so a required variable present in one environment and missing in another is called out. It is not yet enforced at load time, so do not rely on it to fail a boot. If you need that guarantee now, assert on the values you read at startup.
Variables are unlimited on every plan, including Free. The API applies a 1,000 per environment fair-use threshold, and support raises it on request. Values themselves cap at 20,000 characters, names must match a standard identifier pattern, and a partial unique index enforces one live variable per name per environment.
Partly, and the tool is honest about which part. Names and metadata are plaintext, so presence, type and visibility differences are visible while locked. Values are not: identical ciphertext does prove two values are the same, but different ciphertext proves nothing at all, because the same plaintext encrypts differently every time. That case is reported as unknown rather than guessed at.
