The store starts empty. envless/env has to fetch the ciphertext and decrypt it before Env::get() returns anything, and reading a variable before that throws NotLoadedException:
Envless env is not loaded yet. Call Envless\Env\Env::load() first, or prepend vendor/envless/env/register.php.Installing the package loads nothing by itself. There are two ways to make sure the load has happened before your code reads a variable.
Call Env::load() at the top of your entry point
require __DIR__ . '/../vendor/autoload.php';
use Envless\Env\Env;
$record = Env::load();
$debug = Env::boolean('APP_DEBUG');Env::load() returns a plain array<string, string> of every decrypted value, a copy you own and may serialise. It is memoised for the life of the PHP process: later calls return the same array without touching the disk or the network. A load that throws is not memoised, so the next call tries again.
Or prepend register.php
vendor/envless/env/register.php includes Composer's autoloader and calls Env::load(). Point PHP's auto_prepend_file at it and every script is loaded before its first line runs, with no change to your code.
auto_prepend_file = /var/www/app/vendor/envless/env/register.phpphp -d auto_prepend_file=vendor/envless/env/register.php bin/worker.phpIf the load fails, the prepended file throws the same LoadException that Env::load() would, so a misconfigured process stops before it serves a request with a hole in its configuration. PHP-FPM and plain PHP shows the pool and Apache settings.
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 Packagist. The check only runs in the CLI when standard output is a terminal |
Each name is read from $_ENV, then $_SERVER, then getenv(), so a variable set by your shell, your container, an FPM pool or an Apache SetEnv is found wherever PHP put it. A name that starts with HTTP_ is the exception: under PHP-FPM and Apache those slots also carry the request's headers, so for such a name only $_ENV and the process's own environment count, and a client cannot override a variable by sending a header. 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 standalone loader 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 first and then from your Composer project root, the folder that holds vendor/. That second place matters for PHP: PHP-FPM, Apache and php -S run a request with public/ as the working directory, so a .envless next to composer.json is still found.
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, Env::get() reads every value, and the process environment gains every name it is allowed to take.
The process environment
Decrypted values are written into $_ENV, $_SERVER and putenv() without overwriting anything already set in any of them, so a variable from your shell or platform wins there, the way vlucas/phpdotenv behaves by default. Laravel's env(), Symfony's %env()% and libraries that call getenv() keep working. Env::get() always hands back the Envless value.
Names that change how a process or its children execute code are never written into the process environment: 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, whatever their case. PHPRC and PHP_INI_SCAN_DIR matter most here: either one would point a child php at another php.ini, and an ini with auto_prepend_file turns a variable into code. They are still decrypted and readable through Env::get(), and the loader names each one it skipped in a warning: on standard error in the CLI, and through error_log() under PHP-FPM and Apache.
Inside envless run
When envless run 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 reads the store, envless/env adopts those names straight from the environment: no ENVLESS_TOKEN, no key and no request.
envless run --env production -- php artisan queue:workEnv::load() then returns the injected values without fetching. Env::load(revalidate: true) deliberately escapes them and runs a real load, so it needs a machine key like any standalone load.
PHP-FPM starts its workers with an empty environment unless the pool sets clear_env = no, so envless run -- php-fpm alone hands nothing to your requests. Set clear_env = no, or pass each variable with env[NAME] = $NAME in the pool. PHP-FPM and plain PHP has both.
Reloading
Env::load(revalidate: true) skips the memo and the cache, fetches again, and replaces the values Env::get() reads. In the process environment, 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. Under PHP-FPM every request is a new load anyway, so reloading only matters in long-running workers:
pcntl_async_signals(true);
pcntl_signal(SIGHUP, static function (): void {
Envless\Env\Env::load(revalidate: true);
});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 PHP 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 shared rules.
Two rules are PHP's own, because PHP-FPM starts every request from scratch:
| Situation | What envless/env does |
|---|---|
The home folder is not writable, as for www-data on most servers | It caches under the system temp folder instead, in envless-<uid>/cache, a folder it creates with mode 0700 and only uses when your user owns it |
The cache is older than 15 minutes and the API cannot be reached, times out or answers 408, 429 or 5xx | It keeps serving the last cached values and logs [envless] Could not refresh api/production (…), so the variables cached at … were used instead. A Node process would hold its values in memory through the outage; this is how a PHP site does the same |
A refused key, a missing environment or any other 4xx is never covered by the old values, and neither is Env::load(revalidate: true), so revoking a machine key still stops a site at its next load.
What a request costs
| Decryption key | Each request under PHP-FPM, with a fresh cache |
|---|---|
ENVLESS_KEY | One small file read and an AES-GCM decrypt per variable, well under a millisecond in all |
ENVLESS_PASSPHRASE | A request to the API for the workspace, then 200,000 rounds of PBKDF2, around a quarter of a second |
Use ENVLESS_KEY in production. The passphrase is fine for a CLI script or a long-running worker that loads once, and expensive for anything that loads per request.
Servers, workers and scripts
| Process model | Where the load happens |
|---|---|
PHP-FPM, Apache mod_php, php -S | On every request, from the cache. The first request after 15 minutes fetches again |
| Laravel Octane, RoadRunner, FrankenPHP worker mode, Swoole | Once per worker, when it boots your app. Every later request reads the memo |
php artisan queue:work, Symfony Messenger, Horizon | Once per worker process, at boot |
| Cron jobs and CLI scripts | Once per run, at the top of the script or through auto_prepend_file |
When a load fails
Every failure throws Envless\Env\Exception\LoadException, with the API or network error as its previous exception where there is one. The store and the process environment are left as they were, so after a failed reload Env::get() 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: Environment production was not found. | The API refused the request, or never answered and there was no cache to fall back on |
Could not decrypt STRIPE_SECRET. Check ENVLESS_KEY or ENVLESS_PASSPHRASE. | A value the key cannot open |
A build step that runs your code without secrets, such as composer install running package:discover, should not load at all. Env::isConfigured() is true only when a machine key is available, in ENVLESS_TOKEN or the CLI config, or envless run injected the values, so guard the load with it. The session token envless login stores does not count, so a laptop logged in to the CLI still boots from .env:
use Envless\Env\Env;
if (Env::isConfigured()) {
Env::load();
}