A better place for your .env file
Typed, per-environment variables that stay encrypted at rest and reach your process without a plaintext file on disk. When a tool genuinely needs a .env, you can still have one.
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.
What a variable actually carries
The constraints below are enforced in the database and the API, not merely suggested in the UI.
- 3 types
- Stored 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.
- 2 visibilities
- One 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.
- 15 names
- Names like PATH, NODE_OPTIONS and LD_PRELOAD 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.
- 0600
- envless 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.
- 6 diagnostics
- The 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.
- 409
- Bulk 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.
string, number, boolean
server or client
Refused injection outright
File mode on every sync
On .env import
On a concurrent edit
From an existing .env and back again
Import once, then pick whichever delivery mode the tool in front of you actually needs.
- 1
Import the .env you already have
Push reads .env.<slug>, then .env, or whatever you point --file at. It skips bootstrap ENVLESS_ names, encrypts locally, and plans the change against decrypted remote values so you can see exactly what will happen first.
envless push --env production --file .env --dry-run envless push --env production --file .env - 2
Inject into a process, with no file at all
The child gets the decrypted values merged over the ambient environment. Nothing is written to disk, signals are forwarded, and the child's exit code is returned as your own. Pass several environments and the later one wins on a name collision.
envless run --env production -- node server.js envless run --env staging,production -- npm test - 3
Read them typed, in code
envless types generates an envless-env.d.ts from the dashboard schema with no key required, declaring the process.env names as strings and the typed proxies with their real types. The proxy is genuinely read-only: assigning to it throws.
envless types import { env } from '@goenvless/env/server' // every variable, typed and coerced import { env } from '@goenvless/env/client' // only variables flagged client const port: number = env.PORT const debug: boolean = env.DEBUG - 4
Write a real file when a tool insists
Some tooling only speaks .env. Sync writes one per environment and checks the target against git first, warning when the file is tracked. Pass --require-gitignore and it refuses to write anything at all rather than leaving a plaintext secret somewhere git can see it.
envless sync --env production --require-gitignore # writes .env.production, mode 0600, name-sorted, with a generated header - 5
Keep environments honest against each other
Compare pairs two environments by name and reports only-source, only-target, different, same, or an explicit unknown when a value cannot be decrypted on one side. It also diffs type, visibility and required flags, and a one-click copy moves a missing variable across by shipping the ciphertext verbatim.
# in the dashboard: ?compare=<slug>
Read the details
Every delivery mode has a reference page covering its flags, failure modes and exact on-disk behaviour.
- 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.
Environment variables FAQ
Straight answers about how this works in practice.
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.
A thousand, on every plan including Business. That is a soft cap rather than a technical ceiling, and support can raise it. 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.
Explore the rest of Envless
Every capability, broken down the same way.
AI agents
Give a coding agent working secrets with no plaintext .env in the repository for it to read or commit.
TerminalThe CLI
24 commands, browser device login, local encryption, and JSON output on every list.
TypeScriptThe SDK
Zero-dependency typed client, full API coverage enforced by CI, with encryption and webhook helpers.
EncryptionEnd-to-end encryption
AES-256-GCM on your machine, a key the server never sees, and an API that structurally refuses plaintext.
HistoryVersioning and history
Numbered snapshots in one locked transaction, attributed per-variable history, and signed events for every change.
TeamsTeam access control
44 permissions, custom roles, private products isolated to named members, and keys clamped to their creator.
Public feedsVirtual environments
Serve an environment as an encrypted JSON feed at a public URL that needs no SDK, CLI or API key.
SetupGetting set up
Install, sign in, bind the directory, unlock, run. What each step writes, and what it does not.
CloningOne-click duplicate
Clone an environment, project or whole product with every variable, in one transaction, without decrypting anything.
Ship secrets, not chaos.
Start free today and discover why developers trust Envless for end-to-end encrypted, versioned secrets across every environment.