PHP

Load at boot

How envless/env loads: Env::load() or register.php, the machine credentials it reads, the 15 minute cache, envless run, PHP-FPM, reloading in long-running workers, and every exception a load can throw.

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:

txt
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

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

ini
auto_prepend_file = /var/www/app/vendor/envless/env/register.php
bash
php -d auto_prepend_file=vendor/envless/env/register.php bin/worker.php

If 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

VariablePurpose
ENVLESS_TOKENAn ev_sk_ machine key that authenticates the fetch. Required.
ENVLESS_KEYThe base64url raw workspace key, 32 bytes. The values are decrypted locally with it. Required unless ENVLESS_PASSPHRASE is set
ENVLESS_PASSPHRASEThe workspace passphrase, from which the key is derived. ENVLESS_KEY wins when both are set
ENVLESS_PRODUCTThe product slug, when the project slug exists in more than one product. Falls back to .envless
ENVLESS_PROJECTThe project slug. Falls back to .envless
ENVLESS_ENVThe environment slug. Falls back to the first environment in .envless
ENVLESS_API_ENDPOINTOverrides the API base URL, https://api.envless.cloud by default
ENVLESS_DISABLE_UPDATE_NOTICEAny 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.

bash
envless run --env production -- php artisan queue:work

Env::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:

php
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:

SituationWhat envless/env does
The home folder is not writable, as for www-data on most serversIt 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 5xxIt 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 keyEach request under PHP-FPM, with a fresh cache
ENVLESS_KEYOne small file read and an AES-GCM decrypt per variable, well under a millisecond in all
ENVLESS_PASSPHRASEA 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 modelWhere the load happens
PHP-FPM, Apache mod_php, php -SOn every request, from the cache. The first request after 15 minutes fetches again
Laravel Octane, RoadRunner, FrankenPHP worker mode, SwooleOnce per worker, when it boots your app. Every later request reads the memo
php artisan queue:work, Symfony Messenger, HorizonOnce per worker process, at boot
Cron jobs and CLI scriptsOnce 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.

MessageCause
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:

php
use Envless\Env\Env;

if (Env::isConfigured()) {
    Env::load();
}