Environment variables, in full
Every variable either server reads, with its real default, whether it is required, and the ones that do nothing.
This is the table the Environment page is the narrative for: every variable read by either of the two programs this project ships, what it falls back to, whether anything is required at all, and what happens when a value is wrong.
Two programs read a configuration from the environment. The orchestrator is the office — the process that executes runs and hosts the employees and plugins, and the one the other reference pages describe. This site is the documentation and marketplace server you are reading, which needs a Postgres connection and a session secret of its own. Each group below says which of the two reads its rows, because the two have different rules and a variable that works in one will do nothing in the other.
How the values get loaded, and what wins
The orchestrator reads its own .env, by hand, with no dotenv dependency: a line is KEY=value, a leading # is a comment, surrounding single or double quotes are stripped, and a key is only applied when the process environment does not already define it. That last clause is the whole precedence rule, and it is the one people get wrong: a real environment variable always beats the file. The file is read exactly once, at module load, so editing it while the process runs changes nothing — which is invisible enough that the office reports it: /api/health carries a configStale flag and a configStaleDetail sentence, and it fires on either of the two signals it can actually observe, the file being modified and a provider key having appeared in the process environment.
Three behaviours are shared by every read and are worth stating once, because each of them turns a mistake into silence:
- An empty string counts as absent.
DEV3D_LOG_LEVEL=is exactly the same as not setting it. There is no "set to nothing" state to reach for. - A non-finite number silently falls back.
DEV3D_MAX_CONCURRENCY=lotsgives you 4, with no warning. Some numbers are additionally floored or clamped, and those floors are in the tables below. - Paths are resolved against the repository root, not the working directory.
./data/dev3d.sqlitemeans the same file wherever you started the process. This site'sSTORAGE_DIRis the exception and resolves against the process's working directory instead.
This site loads its environment the ordinary Next.js way, and its scripts are run with Node's own flag — node --env-file=.env scripts/seed.ts, which is why the seed and the migration run with no build step. Two of its variables are read through a required() helper that throws while the module is loading, so a missing one stops the process at import time rather than at the first request that needs it. The rest go through an optional() helper with a default.
Both programs then narrow the environment further on a first boot, and the two narrowings are different facts:
| Stage | What it does | What it means for you |
|---|---|---|
| The file, at startup | Fills in only the keys the process environment has not already defined. | Editing .env is enough until the thing that launches the process exports the same name. Then the file is inert. |
| The first boot, into settings | Exactly eight engine-affecting values are copied into the installation's saved settings: workspacesRoot, allowExternalWorkspaces, defaultRoutingPosture, maxConcurrency, softSpendApprovalUsd, autoApproveShell, approvalTimeoutMs and logLevel. | From the second boot on, saved settings win and the environment is ignored for those eight. The console is how they change. Everything else in this page stays environment-only for the life of the process. |
Required
The honest answer for the orchestrator is none: it starts on its defaults with no keys at all, resolves to scripted employees, and badges the mode in the UI. What the table below lists are the variables that are required for something — for this site to start, or for a turn to be billed to a real provider.
| Variable | Required? | Default | What it does |
|---|---|---|---|
DATABASE_URL | Yes — this site | none | The Postgres connection string, and the first thing the site needs. Absent, the module throws Missing required environment variable DATABASE_URL. Copy .env.example to .env and fill it in. It is also read by drizzle.config.ts and by the migration, seed and inspection scripts. |
SESSION_SECRET | Yes — this site | none | The same refusal, with SESSION_SECRET named. What it actually does is in Secrets below, and it is narrower than its own comment in the example file claims. |
ADMIN_EMAIL | Yes — for pnpm seed | none | The owner account's email, lowercased and trimmed. Absent, the seed refuses before touching the database: ADMIN_EMAIL is not set. Add it to .env — see .env.example. |
ADMIN_PASSWORD | Yes — for pnpm seed | none | At least twelve characters, checked by the seed rather than by a form: ADMIN_PASSWORD is not set, or is shorter than 12 characters. Set a real one in .env before seeding; this account owns the installation. There is no default on purpose — a published default is a known credential. |
| a provider key | For a live turn | none | Not required to boot; required for the office to resolve to live on its own. Forcing DEV3D_LLM_MODE=live with no key configured produces the honest warning DEV3D_LLM_MODE=live, but no provider is configured — every turn will fail. |
The asymmetry is deliberate and worth naming: the office treats "no credentials" as a supported configuration, and this site treats two of its variables as non-negotiable. That is not a difference in strictness for its own sake. The office can do something honest without a key — script every employee and say so — while a site with no database has nothing to render and a site with no secret cannot seal the tokens it stores. A configuration that cannot work should fail at boot, and one that can work in a reduced mode should start and explain the mode it is in.
Server and HTTP
| Variable | Required? | Default | What it does |
|---|---|---|---|
HOST | No | 127.0.0.1 | The interface the orchestrator's HTTP and WebSocket server binds to. Loopback by default, so the office is not reachable from the network until you say so. |
PORT | No | 8787 | The orchestrator's port. A bind failure is reported by name: Port 8787 on 127.0.0.1 is already in use. The Vite dev server also reads PORT, as the last fallback for where to proxy /api and /ws. |
SITE_URL | No — this site | http://127.0.0.1:3000 | The public origin. Trailing slashes are stripped. Every absolute URL is built from it: the catalog's downloadUrl, each page's canonical and og:url, the card image URLs, the sitemap and robots entries. |
NODE_ENV | No — this site | unset | production marks the session cookie Secure and arms the SITE_URL warning below. It is the platform's variable, not this project's. |
WEB_PORT | No — the production stack | 3900 | Read by docker-compose.prod.yml, not by the application: the loopback port the web container binds. Not a configuration this site's code ever sees. |
SITE_URL is the variable on this list whose wrong value is quietest: left at its development default on a public host, the marketplace catalog advertises bundle URLs on 127.0.0.1 and every shared link points at a card image that exists only on the machine that built it, with nothing throwing and nothing failing loudly. The server warns once at boot rather than refusing to start, because a local production-mode build is a legitimate thing to do.
Note which of these the orchestrator does not read at all: it never sees SITE_URL, NODE_ENV or WEB_PORT, and this site never sees HOST or PORT as its own bind address. Two programs, two namespaces, one page, because the failure mode of confusing them is a variable that looks set and does nothing.
Database and storage
| Variable | Required? | Default | What it does |
|---|---|---|---|
DEV3D_DB | No | ./data/dev3d.sqlite | The SQLite file holding the office, its runs, stages, turns, tool calls, events and approvals. Environment-only: it has to be known before anything can be read out of it. |
DEV3D_SKILLS_DIR | No | ./skills | The directory the skill loader reads *.md from at boot, created if it is missing. A malformed file is skipped with its reason and is never fatal. |
DEV3D_WORKSPACE | No | ./workspace | The default project directory: what a run uses when it names no floor, and the path the first organisation is created pointing at. It is logged at boot as workspace: …, which is how you confirm it. |
DEV3D_WORKSPACES_ROOT | No | ./workspaces | Where a new project folder is created when you create one by name. Copying this into installation settings on first boot is what stops "create a project" from becoming a way to hand employees the whole disk. |
DEV3D_ALLOW_EXTERNAL_WORKSPACES | No | true | Whether a floor may point at an absolute path outside the workspaces root. Only the literal false disables it — 0, no and off all leave it on. |
DEV3D_MODEL_DISCOVERY_CACHE | No | ./data/model-discovery.json | Where discovered model lists survive a restart. The literal off or none disables the cache, at the cost of a network round trip per provider on every boot. |
DEV3D_POOLED_QUALITY_CACHE | No | ./data/pooled-quality.json | Where pooled benchmark scores are cached. off or none disables it. |
DEV3D_BENCHMARK_CACHE | No | ./data/benchmarks.json | Where the OpenRouter benchmark payload is cached. off or none disables it. |
DEV3D_ENDPOINT_HEALTH_CACHE | No | ./data/endpoint-health.json | Where upstream uptime readings are cached. off or none disables it. |
STORAGE_DIR | No — this site | ./var | Where uploaded media and built plugin bundles are written. Unlike every orchestrator path, it resolves against the working directory of the web process. |
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD | Password, for the production stack | dev3d_net, dev3d, none | Read by docker-compose.prod.yml to provision the stack's own database container. They configure Postgres, not the application; the application only ever sees the composed DATABASE_URL. |
The four cache paths are the group where the interactions matter more than the rows. Each one is disabling, not relocating, when set to off or none: reading and writing stop, and an existing file is left on disk, ignored. That distinction catches people who set a cache off and then look for the file to have gone. Disabling them has a real cost and it is different per cache — no discovery cache is a network round trip per provider per boot, while no endpoint-health cache only means the first lookups are unknown, because lookups are on demand and never block a turn.
Storage is the other place two rules meet. The orchestrator resolves every path against the repository root; this site resolves STORAGE_DIR against its working directory and then funnels every write through a guard that refuses to leave it, which is what makes /marketplace/bundles/<file> able to treat a filename as untrusted input: the name is looked up in the database first and the stored path is re-checked against the storage root, so a traversal attempt fails closed rather than reading an arbitrary file. A cache directory that cannot be written is not fatal either — the orchestrator's store falls back to memory and says so in one line, running without persistence (…); history will be lost on exit.
Models and providers
| Variable | Required? | Default | What it does |
|---|---|---|---|
DEV3D_LLM_MODE | No | auto | mock runs the whole engine against scripted employees with no billing, live requires real providers, and auto resolves to live the moment any provider counts as configured. Unrecognised values fall back to auto. |
DEV3D_ROUTING | No | balanced | The installation's default routing posture — cheap, balanced or quality. Only a default: a floor carries its own, and the engine prefers the floor's. Anything unrecognised becomes balanced. |
DEEPSEEK_API_KEY | For that provider | none | The whole of what makes deepseek configured. Without it the provider is catalogued and its models are not routable. |
OPENAI_API_KEY | For that provider | none | Configures openai and nothing else. It is not the benchmark credential, whatever an earlier page on this site says: pooled benchmark refresh is gated on OPENROUTER_API_KEY. |
OPENROUTER_API_KEY | For that provider | none | Configures openrouter, and is separately the credential the OpenRouter benchmark aggregation needs for pooled quality — one key covering both. |
ANTHROPIC_API_KEY | For that provider | none | Configures anthropic, which speaks the Messages API rather than the OpenAI chat-completions shape. |
DEV3D_DEEPSEEK_BASE_URL | No | https://api.deepseek.com/v1 | Where DeepSeek is reached, version segment included. Overriding it is the supported way to put a proxy in front of a provider. |
DEV3D_OPENAI_BASE_URL | No | https://api.openai.com/v1 | Where OpenAI is reached. |
DEV3D_OPENROUTER_BASE_URL | No | https://openrouter.ai/api/v1 | Where OpenRouter is reached. Requests also carry a referer and a title header, which is what OpenRouter asks gateways to send. |
DEV3D_ANTHROPIC_BASE_URL | No | https://api.anthropic.com/v1 | Where Anthropic is reached. |
DEV3D_LOCAL_BASE_URL | No | http://127.0.0.1:11434/v1 | An OpenAI-compatible local runtime. Its presence — not its value — is what marks the local provider keyless and configured. |
DEV3D_LOCAL_API_KEY | No | none | An optional credential for the local runtime, for a gateway that wants one but does not need a paid key. |
DEV3D_MODEL_DISCOVERY | No | true | Whether the office asks each configured provider what models it serves. Only the literal false turns it off, and mock mode never discovers whatever this says. |
DEV3D_MODEL_DISCOVERY_TTL_MS | No | 21600000 (6 hours) | How long a discovered list stays fresh. 0 re-asks every time. |
DEV3D_POOLED_QUALITY_KEY_VAR | No | ARTIFICIAL_ANALYSIS_API_KEY | The name of the variable holding the pooled-quality key. Only the name is stored, so a secret never reaches a settings document, a log line or the browser. |
DEV3D_POOLED_QUALITY_TTL_MS | No | 604800000 (7 days) | How long a pooled index stays fresh. A week, because a benchmark index moves on the scale of weeks and the free API is rate-limited per key. |
DEV3D_BENCHMARKS | No | true | Whether to read OpenRouter's benchmark aggregation for pooled quality. Only the literal false disables it, and it needs OPENROUTER_API_KEY to do anything. |
DEV3D_BENCHMARK_TTL_MS | No | 86400000 (24 hours) | How long that payload stays fresh. A day, because benchmarks are re-run continuously but an index that matters for routing does not move hour to hour. |
DEV3D_ENDPOINT_HEALTH | No | true | Whether upstream endpoint uptime is tracked as a routing signal. Only the literal false disables it. |
DEV3D_ENDPOINT_HEALTH_PROVIDERS | No | openrouter | A comma-separated list of provider ids whose models are OpenRouter slugs underneath, and are therefore the only ones that can be asked about. Entries are trimmed and empties dropped. |
DEV3D_ENDPOINT_HEALTH_TTL_MS | No | 600000 (10 minutes) | Uptime freshness. Ten minutes, because the underlying figure is a rolling 30-minute one. |
Three interactions in this group are invisible from the rows. First, a provider with no key is filtered out before the router ever sees a model: exclusion happens in the provider registry's routable set, not in scoring, so an unconfigured provider contributes no candidates at all rather than candidates that lose. Its models still appear in the catalogue and the Models view, flagged with a hint naming the variable to set. Second, the keyless rule is about a base URL rather than a key, which is what lets the office be pointed at an Ollama, vLLM or LM Studio instance with no credential — and it is also the rule a plugin reproduces with "keyless": true and a loopback http URL. Third, the pooled-quality key is indirect: the value of DEV3D_POOLED_QUALITY_KEY_VAR is looked up in the environment when a refresh runs, and when it is empty no request is made at all, so a default install makes no outbound call it did not have to.
Discovery is where the split between membership and metadata lives, and it decides which cache path you are debugging. Membership comes from the provider; tier, price, quality and capabilities come from the curated catalogue that ships with the server. So discovery can add a model nobody has described — it stays routable, flagged as unrated — and it can withdraw a model the provider no longer serves, which is better than leaving it in the list to fail a turn. What it cannot do is invent a price or a tier. And because every TTL here is clamped at zero rather than rejected, a negative number behaves exactly like 0: re-ask every time.
Budget and limits
| Variable | Required? | Default | What it does |
|---|---|---|---|
DEV3D_SOFT_SPEND_APPROVAL_USD | No | 1.5 | Ask a human before a run's spend crosses this, in USD. 0 disables the gate. It fires at most once per run, only between stages, and only once spend has already crossed. |
DEV3D_RUN_BUDGET_USD | No | 5 | Read into the config and never referenced again. A run's real ceiling is the floor's budget.defaultRunUsd, which also defaults to 5 — see Variables that are read but do nothing. |
DEV3D_MAX_CONCURRENCY | No | 4, clamped to 1..16 | How many employees may execute at once inside one stage. It applies to parallel branches, review-loop reviewers and review-loop revisions — nothing else in the engine is concurrent. |
DEV3D_APPROVAL_TIMEOUT_MS | No | 600000, floored at 1000 | How long any approval waits before the engine treats it as refused. There is no ceiling. |
These four are the money-and-permission surface, and three of them interact in ways that only show up mid-run. The soft gate and the hard ceiling are different mechanisms with different outcomes: the hard ceiling is the floor's budget, it is checked before each stage on >=, and exceeding it fails the whole run with Run budget of $X was exhausted before stage "Y". The soft gate raises an approval of kind 'spend' with a null turnId, attributed to the first stage's owner so it has somewhere to appear; approving continues to the real ceiling, and refusing sets the run to cancelled — not paused, and not awaiting-approval, which is a stage status that only tool approvals set.
The connection between the timeout and the gate is the one worth internalising: the approval that the soft gate raises is subject to DEV3D_APPROVAL_TIMEOUT_MS like any other, so an operator who never answers gets a cancelled run rather than a run that waits forever. And because abortReason is consulted between turns only by debate and review-loop, the ceiling is a per-stage check rather than a per-turn one: a single or parallel stage already running can overshoot it before the next stage boundary notices. Concurrency is bounded by the same shape — one bounded map — so raising DEV3D_MAX_CONCURRENCY does nothing for a stage that runs one employee.
Plugins and the marketplace
| Variable | Required? | Default | What it does |
|---|---|---|---|
DEV3D_PLUGINS_DIR | No | ./plugins | The bundled plugin directory. Every immediate subdirectory holding a plugin.json is one plugin, loaded with source label bundled. |
DEV3D_PLUGIN_INSTALL_DIR | No | ./data/plugins | Where downloaded plugins land, loaded with source label marketplace. Kept out of the bundled directory so a download cannot be confused with something that shipped. |
DEV3D_ALLOW_PLUGIN_INSTALL | No | false | The install gate. Only the literal true opens it; until then the host refuses before it fetches anything. |
Which directory a plugin was found in decides more than a label. A bundled plugin can be disabled but never deleted through the API — the refusal is This plugin ships with the office in DEV3D_PLUGINS_DIR. Disable it, or remove the directory yourself. — and a marketplace upgrade may only replace a plugin that came from DEV3D_PLUGIN_INSTALL_DIR. That is what stops an upgrade from silently replacing the files a checkout ships with, and it is why installing a bundle for a plugin that already ships as bundled is refused rather than merged.
The gate is the sharpest of the three, because installing unpacks an archive and, if its manifest declares an entry, imports someone else's module into the orchestrator's own process. The decision therefore belongs to the environment the process was launched from: it is deliberately not one of the eight values copied into installation settings, so no console switch can turn it on. Registering a marketplace to browse is not gated at all — reading a catalogue document is not the same act as running what it points at. The manifest rules are on Plugin manifest, and the bundle rules in The marketplace lifecycle.
Front-end and analytics
| Variable | Required? | Default | What it does |
|---|---|---|---|
DEV3D_SERVER_HOST | No — dev server only | 127.0.0.1 | The host the Vite dev server proxies /api and /ws to. Read by apps/web/vite.config.ts; the orchestrator never reads it. |
DEV3D_SERVER_PORT | No — dev server only | process.env.PORT ?? '8787' | The port that proxy targets. This is the variable to set when you started the orchestrator somewhere other than 8787 and do not want to edit the config by hand. |
VITE_WS_URL | No — browser bundle only | none | A hard override of the browser's WebSocket URL, instead of deriving it from the page origin. Only needed when the UI is served from somewhere other than the orchestrator. |
NEXT_PUBLIC_MATOMO_TAG_MANAGER_URL | No — this site | a production container URL | The Matomo Tag Manager container, loaded on the public site only after a visitor allows analytics. off, none or an empty string ships no tag manager at all. |
All four of these live on the wrong side of a process boundary from the thing they configure, and each fails silently in its own way. DEV3D_SERVER_HOST and DEV3D_SERVER_PORT are read by the Vite config at dev-server start, so a value that is wrong there produces a browser talking to nothing rather than a server refusing to boot. VITE_WS_URL and NEXT_PUBLIC_MATOMO_TAG_MANAGER_URL are inlined into the browser bundle at build time, which means setting either of them on a running server does nothing at all — a rebuilt bundle is the only way to change them, and because the value is in the page source it is never a secret. Neither program reads the other's names: the orchestrator has no idea what NEXT_PUBLIC_ means, and the site ignores every DEV3D_* variable.
Analytics is the one place on this page with a consent model rather than a switch, and the details are worth stating because they are load-bearing for what the site claims about itself. The container is the only third-party script on the site. It is loaded only after a visitor opts in, and the decision is a first-party cookie, dev3dnet_consent, honoured for 180 days — first-party and not localStorage specifically so the server can read it while rendering, which means the tag is either in the first paint or absent from it, with no flash of an unwanted script. Declining is recorded the same way as accepting, so a visitor who says no is not asked again. A configured value that is not https is refused with a warning rather than loaded, because a tag manager over plain http would be blocked as mixed content on an https page, and discovering that silently is the worst way to find out.
Seeding and administration
| Variable | Required? | Default | What it does |
|---|---|---|---|
PLUGIN_SOURCE_DIR | No — this site | E:\Development\dev3d\plugins | Where pnpm seed imports listings from: it builds a .tar.gz per plugin directory, computes the sha256 and publishes the listing. The default is a path on the machine this checkout was written on, which is exactly why the production example file overrides it. |
OWNER_LINKS | No — this site | https://github.com/scarecr0w12 | Comma-separated profile links for the owner account, seeded as the person's external identities and published as Person.sameAs. Anything not an http(s) URL is dropped rather than stored. |
--with-demo-traffic | No — a flag, not a variable | off | Appended to the seed command to generate demonstration traffic and download rows. Listed here because it is the one administrative option that changes what the seed writes, and it is not an environment variable at all. |
Administration is asymmetric between the two programs, and this is the group where that shows. The orchestrator has no seeding step: on boot it creates its default organisation, its thirteen roles and its three pipelines if the database does not already have them, and the eight values named earlier are the only environment it carries forward into settings. Anything else administrative — hiring, firing, tools, skills, budgets, posture, plugin enablement — is a console action or a socket command, not a variable. This site is the opposite: roles, permissions, the owner account, site settings, documentation and the shipped plugin listings are all written by an idempotent pnpm seed, which keys on rows that already exist and updates rather than duplicating them, and which is safe to re-run.
Two details of that seed are easy to trip over. It will not reset an existing owner's password — it only fills in a missing one — so a forgotten owner password is a database operation rather than a re-seed. And the owner credentials are required rather than defaulted, for the reason the code states plainly: a published default is not a convenience, it is a known credential, and anyone who cloned the repository would know the password of the account that owns the installation.
Variables that are read but do nothing
A variable can survive in a source tree for a long time after the code that consumed it has gone, and the only honest way to write this section is to check each one rather than assume. The list below is short, and that is the finding.
DEV3D_RUN_BUDGET_USDis read, parsed and stored asconfig.runBudgetUsd— and then referenced nowhere else, in the orchestrator or in the core package. Setting it changes nothing whatsoever. The reason it has gone unnoticed is a coincidence of defaults: it defaults to5and so does the floor'sbudget.defaultRunUsd, which is the value the engine actually reads, soDEV3D_RUN_BUDGET_USD=5appears to work. Set it to 25 and the ceiling stays where the floor put it; change the floor's budget in the console and the ceiling moves. This is the one entry on this page that is a real trap rather than a footnote.DEV3D_TRACElooks like a variable and is not one. It is a flag set onglobalThisby the web verification harness, never read fromprocess.env, so exporting it has no effect on any process.
One setting that looks equally live is not an environment variable at all, and belongs beside the one above rather than in a table: repairNote is declared and rendered into a prompt but never passed by any caller, so the turn-retry path that would use it does not exist. It used to have company in maxTurnsPerStage — declared in the role type, seeded for all thirteen roles and copied through, and never read — but that one has since been enforced: a role that reaches its per-stage turn cap is skipped for the remainder of the stage, and the skip is logged by name with the remedy beside it. So the console's per-stage turn number is now a real control, and repairNote is the only remaining knob of that shape. Known gaps tracks it with the rest.
A name surviving in the source is not evidence that it is wired to anything. The presence of a variable in an example file, a comment or a config type says only that somebody once intended it. The test is a reference on the consuming side, and the only way to run it is to read the code — which is what this page is, and why it says "read but unused" about a single variable rather than claiming that everything else is live.
Secrets
Five names on this page are credentials, and they are all read once into memory at boot and never sent anywhere. The four provider keys — DEEPSEEK_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_API_KEY — plus the optional DEV3D_LOCAL_API_KEY, and one indirect fifth: the variable named by DEV3D_POOLED_QUALITY_KEY_VAR, which authorises an outbound benchmark lookup and nothing else. On this site's side there are three more: SESSION_SECRET, ADMIN_PASSWORD, and — if you deploy the production compose stack — POSTGRES_PASSWORD.
| Secret | What a leak costs | What rotating it breaks |
|---|---|---|
| A provider key | Spend. Every model that key can reach is billable to you, and a leaked key is typically found when the invoice arrives rather than before. | Nothing but the next boot. Keys are read from the environment at startup, so replacing one and restarting is a complete rotation. |
SESSION_SECRET | Less than it sounds, and the reason is worth knowing: nothing is encrypted with it in a browser-visible way, and it does not sign the session cookie — the cookie's value is a random token looked up by an unsalted hash. What it does protect is the encrypted GitHub access tokens this site stores for release imports. | Every stored GitHub connection. The value is sealed with AES-256-GCM under a key derived from this secret, so a rotation makes each one undecryptable, and the next sync says so: The stored access token could not be decrypted, which means SESSION_SECRET changed. Re-enter the token. It also changes the daily salt behind the analytics visitor hash, which is a one-day blip in distinct-visitor counts rather than a loss. |
ADMIN_PASSWORD | The installation. This account owns the site: it can write documentation, publish listings and grant verification and trust. | Nothing. It is used once, to create a password hash; changing it in the environment afterwards changes nothing, and the password is changed through the account page. |
Three consequences follow, and the first is the one that matters most in practice.
.envmust never be committed. It is the file that holds every credential above, and it is written for a machine that is yours. Both repositories ignore it and both ship an example file with the same keys and no values; the production example goes further and tells you to set the file's mode to 600, because on a server that file holds the credentials for a public deployment rather than for a laptop.- A manifest can never carry a secret. A plugin declares a provider with the name of an environment variable in
keyEnvVar; the server reads that variable from its own process environment. So a marketplace bundle is safe to publish and to inspect, and a provider whose variable is unset is catalogued but contributes no routing candidates. The same rule explains whyDEV3D_POOLED_QUALITY_KEY_VARholds a name rather than a key: the only thing that ever reaches a settings document, a log line or a browser frame is the name. - The secrets that reach a database are the ones to think about. Passwords and session tokens are hashed with scrypt and sha256 respectively, because nothing needs to read them back. A GitHub token is the exception, and the code says so rather than claiming otherwise: it has to be presented to GitHub on every sync, so it is encrypted at rest with a self-describing
v1.<iv>.<tag>.<ciphertext>value and a key derived fromSESSION_SECRETthrough a distinct label, so that a weakness in one use of that secret does not hand over the key for another.
If something here contradicts what the running server does, the server is the tie-breaker. GET /api/health reports the resolved mode and the reason for it, GET /api/providers says which providers count as configured and carries the hint for each one that does not, and GET /api/plugins reports whether the install gate is open. Those three endpoints between them answer most "why is it behaving like that" questions about configuration without reading a single file. If a variable you are using is genuinely missing from this page, that is a documentation bug rather than evidence that the variable does nothing — tell us.
Where to go next
Linked from
Did this page answer your question?