The store starts empty. envless-env has to fetch the ciphertext and decrypt it before Envless.env holds anything, and reading a variable before that raises Envless::Env::NotLoadedError:
Envless env is not loaded yet. Call Envless::Env.load, or require "envless/env/load", first.Requiring envless/env loads nothing by itself. There are two ways to make sure the load has happened before your code reads a variable.
Require envless/env/load first
envless/env/load loads when it is required. Make it the first require of your entry point, config.ru, a worker's boot file or a script, and everything after it sees a loaded environment.
require "envless/env/load"
database_url = Envless.env[:DATABASE_URL]If the load fails, the require raises the same Envless::Env::LoadError that Envless::Env.load would, so a misconfigured process stops at boot instead of serving requests with a hole in its configuration. In Rails, the Railtie does this for you before the app is configured.
Or call Envless::Env.load yourself
require "envless/env"
record = Envless::Env.load
debug = Envless.env[:DEBUG]Envless::Env.load returns a plain Hash of every decrypted value as Strings, a copy you own and may serialise. It is memoised: later calls return the same values without fetching again, and calls from several threads wait for one load instead of starting their own. A load that raises is not memoised, so the next call tries again.
The credentials it reads
| Variable | Purpose |
|---|---|
ENVLESS_TOKEN | An ev_sk_ machine key that authenticates the fetch. Required. |
ENVLESS_KEY | The base64url raw workspace key, 32 bytes. The values are decrypted locally with it. Required unless ENVLESS_PASSPHRASE is set |
ENVLESS_PASSPHRASE | The workspace passphrase, from which the key is derived. ENVLESS_KEY wins when both are set |
ENVLESS_PRODUCT | The product slug, when the project slug exists in more than one product. Falls back to .envless |
ENVLESS_PROJECT | The project slug. Falls back to .envless |
ENVLESS_ENV | The environment slug. Falls back to the first environment in .envless |
ENVLESS_API_ENDPOINT | Overrides the API base URL, https://api.envless.cloud by default |
ENVLESS_DISABLE_UPDATE_NOTICE | Any value silences the one-time check for a newer envless-env on RubyGems. The check only runs when stdout is a terminal |
Get the machine key from Keys in the dashboard and the raw workspace key from machine access. With ENVLESS_TOKEN unset, the loader falls back to the token envless login stored on this machine, and since that is a session token it fails with ENVLESS_TOKEN must be a machine key (ev_sk_…). Create one in the Envless dashboard. The runtime is always machine authenticated.
The .envless file is the one envless link writes into your project folder. The loader reads it from the current working directory, so start your process from the folder that holds it, or set the three slugs as variables instead.
What a load does
Resolve the target and the key
The token, then the product, project and environment, then the key. With ENVLESS_PASSPHRASE, the loader asks the API which workspace the token belongs to, because the key is derived from the passphrase and a salt bound to that workspace. With ENVLESS_KEY there is no request at this step.
Read the cache, or fetch
A cache entry under 15 minutes old is decrypted in memory and nothing is fetched. Otherwise the ciphertext is fetched, 100 variables per request, and written back to the cache.
Decrypt and fill the process environment
Every value is decrypted in memory. If one will not decrypt the load fails naming it, and nothing is populated. On success, Envless.env holds every value, and ENV gains every name it is allowed to take.
The process environment
Decrypted values are written into ENV without overwriting anything already set, so a variable from your shell or platform wins there, the way dotenv behaves by default. Gems that read ENV directly, such as pg reading DATABASE_URL or the AWS SDK, keep working, and child processes inherit the values. Envless.env always hands back the Envless value.
Names that change how a process or its children execute code are never written into ENV, the same twenty-six @goenvless/env and envless run refuse, whatever their case: NODE_OPTIONS, NODE_PATH, NODE_REPL_EXTERNAL_MODULE, ELECTRON_RUN_AS_NODE, LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, PATH, BUN_INSPECT, BUN_INSPECT_CONNECT_TO, RUBYOPT, RUBYLIB, BUNDLE_GEMFILE, GEM_HOME, GEM_PATH, PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE, PHPRC, PHP_INI_SCAN_DIR, ENVLESS_CLIENT_ENV and ENVLESS_INJECTED. RUBYOPT alone would let anyone who can edit a variable make every Ruby process on the machine require a file of their choosing. They are still decrypted and readable through Envless.env, and the loader warns on stderr naming each one it skipped. A name or a value the operating system cannot hold in an environment variable, one with a = or a NUL byte in it, is skipped the same way.
Inside envless run
When envless run or envless build starts your process, the CLI has already decrypted the values into the environment and written a manifest, ENVLESS_INJECTED, naming each variable with its type and visibility. The first time anything touches the store, envless-env adopts those names straight from ENV: no ENVLESS_TOKEN, no key and no request.
envless run --env production -- bundle exec pumaEnvless::Env.load then returns the injected values without fetching. Envless::Env.load(revalidate: true) deliberately escapes the seed and runs a real load, so it needs a machine key like any standalone load. See typed variables inside envless run for the manifest in detail. It is the same for every runtime.
Reloading
Envless::Env.load(revalidate: true) skips the memo and the cache, fetches again, and replaces the values in Envless.env. In ENV, a name a previous load wrote is updated, a name that has disappeared is removed, and a name that was already set from somewhere else is still left alone.
require "envless/env"
Signal.trap("HUP") do
Thread.new { Envless::Env.load(revalidate: true) }
endThe reload runs in a thread because a trap handler cannot take the locks a load uses.
The cache
After a fetch, the ciphertext is written to ~/.envless/cache/, one file per workspace, product, project and environment, in the same format @goenvless/env writes, so a Node, a Python and a Ruby app on one machine share it. Only ciphertext is stored, never a decrypted value, in a folder with mode 0700 and files with mode 0600. Writing it is best effort: a failed write never fails the load. An entry older than 15 minutes, stamped in the future or written under another workspace is ignored. envless cache clear empties it. Caching and reloads covers the details, which are identical in every runtime.
After a fork
Puma in cluster mode, Unicorn and Sidekiq's swarm fork worker processes. When the app is loaded before the fork, with Puma's preload_app! or Unicorn's preload_app true, every worker inherits the load: the values are already in Envless.env and ENV, and no worker fetches again. Without preloading, each worker loads on its own, and all but the first read the ciphertext cache instead of the API. A reload in one worker reloads only that worker.
When a load fails
Every failure raises Envless::Env::LoadError, with the API or network error as its cause where there is one. It is a StandardError under Envless::Error, not Ruby's own ::LoadError for a file require cannot find, so rescue it by its full name. The store and ENV are left as they were, so after a failed reload Envless.env keeps the values of the last load that worked.
| Message | Cause |
|---|---|
Missing ENVLESS_TOKEN. Set it to an Envless machine key (ev_sk_…) to load variables. | No token in the environment or in the CLI config |
ENVLESS_TOKEN must be a machine key (ev_sk_…). Create one in the Envless dashboard. | A session token, which the API refuses here |
Missing ENVLESS_PROJECT. Set it to your project slug, or add a .envless file. | No project slug |
Missing ENVLESS_ENV. Set it to your environment slug, or add a .envless file. | No environment slug |
Set ENVLESS_KEY (a workspace key) or ENVLESS_PASSPHRASE to decrypt in machine mode. | Neither decryption credential |
ENVLESS_KEY is invalid: Invalid 256-bit key: got 16 bytes. | A raw key of the wrong length |
Could not load variables for api/production: Project not found. | The API refused the request, or never answered |
Could not decrypt STRIPE_SECRET. Check ENVLESS_KEY or ENVLESS_PASSPHRASE. | A value the key cannot open |