Environment variables

Every variable the platform reads from .env, grouped by subsystem — which ones are required, which ones the code reads but the template never declares, and which ones nothing reads at all.

9 min readUpdated 8 August 2026env, configuration, secrets

Configuration lives in one file: .env at the repository root, next to package.json. .env.example is the template the installer copies when no .env exists.

The backend loads it before any other module runs, probing four paths in order and stopping at the first that exists:

<cwd>/.env          # the repo root — this is the one you edit
<backend>/../.env
<backend>/.env
<cwd>/../.env

If none is found it falls back to the ambient process environment, which is how a container deployment can supply everything without a file at all.

The installer sets chmod 600 .env. Keep it that way — the file holds your database password, four session-signing secrets, every payment credential, and the passphrase that unlocks custodial wallet keys.

Editing it safely

pnpm env-manager is a targeted line editor for this file. It replaces one line at a time, so comments, section headers, ordering and quoting survive; a round-trip through a .env parser strips all of that.

node scripts/env-manager.mjs get --json
node scripts/env-manager.mjs set APP_TWILIO_AUTH_TOKEN=abc123 --restart

Every write snapshots a timestamped .env.bak and renames a temp file into place. With --restart it drains the backend, restarts, health-checks it, and rolls back to the snapshot if the process does not come back healthy. Secret-looking keys are redacted on read, so get reports set/unset rather than values.

The tool refuses to edit ENCRYPTED_ENCRYPTION_KEY and ENCRYPTION_KEY_PASSPHRASE at all. Changing either permanently bricks every encrypted wallet on the install.

Two rules that decide whether an edit takes effect

Anything named NEXT_PUBLIC_* is inlined by Next.js at build time. Editing it and restarting changes nothing in the browser.

NEXT_PUBLIC_SITE_URL is the worst case: every client API call falls back to it (frontend/lib/api.ts), and its hostname is baked into images.remotePatterns in next.config.js. Move the platform to a new domain without running pnpm build:frontend and the browser keeps calling the old origin while next/image rejects every image served from the new one.

Everything else is read when a process starts. pnpm restart picks it up — pnpm stop && pnpm start, which parks the site on the maintenance server in between.


Application

The canonical public origin. In production it is the entire CORS allowlist — the backend derives http/https and www/non-www variants from this value and nothing else, so an unset value produces an empty allowlist and every browser request fails. Also the base for client API calls and the only hostname next/image will optimise. Changing it needs a frontend rebuild.
Shown in the header, page titles and outbound email. Unset, the fallback depends on the reader — Bicrypto in the PM2 config and most components, My App in the root layout's page titles, App in the PWA manifest — so set it explicitly rather than relying on any of them.
Meta description for the public pages.
Demo sites only. When true, EVERY new registration is given the Admin role — email/password and Google signup alike, on production builds too. Demo mode also blocks admin writes for anyone who is not Super Admin, and narrows scheduler refusal alerts to Super Admins so a refused cron job does not email every visitor. Leave it false on a real deployment.
production on a live install. It is what makes session cookies Secure + SameSite=None, so a production build served over plain HTTP cannot log anyone in. It also drops localhost origins from the CORS allowlist.
Declared for reference only. The frontend PM2 app hardcodes PORT: 3000 in its own env block, and a PM2 env block beats the process environment, so editing this does not move the frontend.
The port the backend binds. PORT is ignored — this is the only variable that moves it. The cron app deliberately sits on 4001; nothing should connect there.
Worker count for the threaded entry point only (pnpm start:thread). Clamped to the CPU count. Read by backend/thread.ts and production.thread.config.js; no application code reads it.
Locale used when the visitor has expressed no preference.
Comma-separated locale codes offered in the language switcher.
dark, light or system.
Google OAuth client ID. The backend verifies Google ID tokens against it on both login and registration, so a mismatch between this and the value the button was built with rejects every Google sign-in.
Development only. Extra IPs allowed to reach the dev server from other devices on the LAN, comma-separated. Read by frontend/next.config.js; has no effect on a production build.
Not in the template on purpose. Unset or inline means one process both serves HTTP and runs the scheduler; off registers no jobs; only runs jobs and serves no traffic. production.config.js sets off on the backend app and only on the cron app, so the split is already whole. Set CRON_MODE="inline" in .env to collapse back to one process — production.config.js reads it and drops the cron app entirely.

Database

Schema name. The installer prompts for it and writes it here.
MySQL user.
MySQL password. backend/config.js treats an empty value as missing and logs a boot error, though it does not stop the process; the database backup and restore endpoints coerce it to an empty string instead. Set a real password.
Database host.
Database port.
Schema sync mode. lazy (the default) only alters tables when the model fingerprint in backend/.sync-hash changed. none authenticates and touches nothing — the setting to reach for when you are diagnosing foreign-key churn. always forces a full ALTER sync, for a schema that drifted outside Sequelize. force DROPS and recreates every table and loses all data.

It is not a repair mode. It drops every table and recreates it empty. If you are trying to fix a schema that no longer matches the models, always is the escape hatch.

Sessions and token secrets

All four are 128-hex-character values. The installer generates them with crypto.randomBytes(64) on a fresh install. .env.example ships real-looking sample values — replace them.

There is no fallback and no default. Any secret that is unset or shorter than 32 characters makes the route that needs it fail with a 500 at the moment it is used, not at boot, so a bad APP_RESET_TOKEN_SECRET looks like "password reset is broken" rather than "the platform will not start".

Signs the short-lived access token on every session.
Signs refresh tokens. Rotating it logs everyone out.
Signs password-reset tokens.
Signs email-verification tokens.
Access-token lifetime. Only the suffixes s, m, h, d parse; anything else throws a 400 on login. The template ships 30m; the code default when the variable is absent is 15m.
Refresh-token lifetime, and therefore how long an idle session survives.
How long a password-reset link stays valid.
LEAVE UNSET for a proxy on this machine. The backend honours a forwarding header whenever the connection came from loopback, so nginx and Apache work with no configuration. Set "true" only for a load balancer on a different host — it then believes a forwarding header from any peer, which is dangerous while the API port is reachable directly. "false" disables the header entirely (diagnostics only) and collapses every visitor into one rate-limit bucket.
Networks other than loopback whose requests may carry a forwarding header — comma-separated addresses or CIDRs, e.g. 10.0.0.0/8. The safe way to trust a proxy on another host. Also unlocks the single-value CDN headers (CF-Connecting-IP, True-Client-IP, X-Real-IP), which are ignored by default because Apache and nginx forward them straight through from the client.

Rate limiting

Per-IP cap on MUTATING requests (POST, PUT, PATCH, DELETE) per window. GETs are not counted.
Window length in seconds. The legacy spelling RATE_LIMIT_EXPIRY is still honoured for installs that already set it, but this name wins.
Per-IP cap for HMAC-signed Hummingbot bot traffic, applied only when a full signature header set is present. Must be at least the addon's Trade budget (600/min) or that budget cannot be delivered.
Failed HMAC verifications one IP may accumulate per minute before the bot-sized allowance is withdrawn and it drops back to RATE_LIMIT.

Redis

Redis is a hard boot dependency, not a cache. Sessions, CSRF tokens, rate-limit counters, distributed locks, the BullMQ scheduler and cross-process settings invalidation all live in it. The backend exits with code 78 (EX_CONFIG) when it is unreachable, printing the host and port it tried. Every PM2 config lists 78 in stop_exit_codes, so PM2 stops the app instead of crash-looping it.

sudo apt-get install -y redis-server && sudo systemctl enable --now redis-server
redis-cli -h 127.0.0.1 -p 6379 ping    # expects: PONG
Redis host.
Redis port.
Leave empty for an unauthenticated local Redis.
Logical database index. Read by the connection code and the queue workers but not declared in .env.example.

Mail

Which transport sends mail: nodemailer-service, nodemailer-smtp, nodemailer-sendgrid or local.
The address shown to recipients.
Display name on outbound mail.
For nodemailer-service: the well-known provider, e.g. gmail or outlook.
Mailbox address for the service transport.
App password, not the account password. Gmail and Outlook both reject the real one.
SMTP server hostname.
SMTP port. 587 pairs with tls, 465 with ssl.
tls for STARTTLS on 587, ssl for implicit TLS on 465. Mismatching this with the port produces a connection that hangs rather than a clear error.
From address for the SMTP transport, and the login user unless APP_NODEMAILER_SMTP_USERNAME is set.
SMTP password.
SendGrid API key, for the nodemailer-sendgrid transport.
Verified SendGrid sender address.
Path to the local sendmail binary, for the local transport. Find it with which sendmail.

.env.example ships APP_EMAILER="nodemailer-smtp" with port 587 and tls. If you delete those lines rather than filling them in, the code defaults take over — nodemailer-service, smtp.gmail.com, port 465, ssl — and mail silently goes nowhere. Set every mail variable explicitly.

SMS

Twilio delivers every SMS the platform sends: login and 2FA codes, phone verification, withdrawal and password-change codes, and notification messages. The provider refuses to initialise unless the account SID starts with AC and either a phone number or a messaging service SID is present.

Twilio account SID. Must start with AC.
Twilio auth token.
Sending number in E.164 form. Either this or a messaging service SID is required.
Messaging service SID, as an alternative to a single sending number.
Routes only one-time codes to a different provider. Empty means Twilio sends codes too; msg91 moves codes to MSG91 while Twilio still sends everything else. MSG91 cannot carry free-text notifications — its send API requires a registered template, and DLT caps each template variable at about 30 characters.
MSG91 API key, from Settings → API Keys. NOT the tokenAuth from an OTP Widget snippet: that is a public browser token, MSG91 rejects it, and sends still report success. Verify at Admin → System → SMS Providers.
Optional sender ID. Without one MSG91 uses its shared sender; your own is only needed past roughly 2,000 messages a month in a country, or for branding.
Template ID from OTP → Add template, using ##OTP## as the placeholder. OTP templates are approved instantly.
India only. DLT template entity ID, if your MSG91 account requires it.

Push notifications

Firebase project ID for native mobile push. A non-empty value switches the FCM channel on, so a placeholder here half-enables it and fails at boot. Leave blank to disable.
Firebase service-account private key.
Firebase service-account client email.
Path to a service-account JSON file, as an alternative to the three fields above.
VAPID public key for browser push. Works without Firebase, in Chrome, Firefox, Edge and Safari. Generate a pair with pnpm vapid:generate.
VAPID private key.
Contact address browsers show for push, as a mailto: URI.

Exchange providers

Which exchange is live is a database row set from Admin → Finance → Exchange Providers, not an environment variable. The backend then builds the credential names from the provider alias at runtime:

APP_${PROVIDER}_API_KEY
APP_${PROVIDER}_API_SECRET
APP_${PROVIDER}_API_PASSPHRASE

So a grep for APP_BINANCE_API_KEY in the source finds nothing even though the variable is load-bearing. Add the trio for whichever provider you activate.

First three letters of the exchange alias — bin, kuc, kra, okx, xt. Read only by the frontend chart and market-data code; zero backend readers, so it selects the chart symbols, not the trading connection.
KuCoin API key.
KuCoin API secret.
KuCoin API passphrase. KuCoin is one of the providers that needs all three.
Binance API key.
Binance API secret.
XT API key.
XT API secret.

Fiat exchange rates

Every configured provider is queried each run and the results are merged, so coverage is the union — a currency one source is missing is still priced by another. The keyless providers alone cover roughly 159 of 160 currencies.

Comma-separated provider IDs in priority order: openexchangerates, exchangerate-api, open-er-api, currency-api, frankfurter. Providers whose key is absent are skipped automatically. Leave unset to use all of them.
How a currency several sources carry is resolved. consensus takes the largest cluster of agreeing sources, which guards against a stale primary — OpenExchangeRates was observed serving SSP at 130 while three other sources agreed on ~4900. priority always takes the earliest-listed provider that has it. Either way, disagreement above 2% is logged with every source's value.
Deprecated single-provider selection. Still honoured, but it pins that provider to the front of the priority list rather than disabling the others. Prefer APP_FIAT_RATES_PROVIDERS.
OpenExchangeRates app ID.
ExchangeRate-API key.
Comma-separated CODE=units-per-USD overrides, for codes reused after a redenomination where sources disagree about which unit the code names. CODE=retired drops the currency. Read by the rate merger but not declared in .env.example.

Deposit gateways

Each gateway's readiness is computed from these variables, not from the database row — the credential names in backend/src/utils/deposit-gateway/registry.ts are read straight out of process.env, and Admin → Finance → Deposit → Gateways reports a gateway as unconfigured until they are present. All are optional: leave blank for any gateway you do not enable.

Base URL several gateways use to build return and webhook URLs. Set it to your public origin.

Stripe, PayPal, Paystack

Stripe publishable key.
Stripe secret key.
PayPal client ID. Public — it reaches the browser.
PayPal client secret.
Paystack secret key.
Paystack public key.
true for the Paystack test environment.
Where Paystack sends the customer after payment.
Paystack webhook target.

TransFi (fiat on/off-ramp)

Sandbox and production credentials are not interchangeable: sandbox credentials return UNAUTHORIZED_CUSTOMER against api.transfi.com, and vice versa.

TransFi API username, from Displai → Settings → Integration.
TransFi API password.
TransFi merchant ID.
Shared secret used to verify inbound webhook signatures.
Explicit environment override rather than deriving from NODE_ENV, for the reason above. https://sandbox-api.transfi.com or https://api.transfi.com. TransFi's own auth docs print api-sandbox.transfi.com; that host does not resolve, so do not "fix" this value to match them.
purposeCode sent on every order. Must be one of the 58 values TransFi accepts.
Required when the purpose code is other. Minimum 10 characters.
Pin the webhook HMAC canonicalisation once you have observed it in an environment: raw (recommended) or python. Unset means try raw, then fall back and warn.
false forces production when the base URL is unset.
Per-request timeout in milliseconds.
How long currency and method discovery is cached, in milliseconds.
How long a first deposit waits inside the request for TransFi to finish screening a new payer, in milliseconds.
Retry hint handed back to the client, in seconds.

The other twelve gateways

2Checkout merchant code.
2Checkout secret key.
2Checkout account reference.
Adyen API key.
Adyen client key, used by the browser component.
Adyen merchant account name.
Adyen HMAC key for webhook verification.
test or live.
Authorize.Net API login ID.
Authorize.Net transaction key.
Authorize.Net signature key for webhook verification.
dLocal login. dLocal picks sandbox or production from NODE_ENV, not from a flag of its own.
dLocal transaction key.
dLocal secret key.
eWAY API key.
eWAY API password.
iPay88 merchant code.
iPay88 merchant key.
Klarna API username.
Klarna API password.
Klarna webhook shared secret.
Mollie API key.
Where Mollie returns the customer.
Mollie webhook target.
PayFast merchant ID.
PayFast merchant key.
PayFast passphrase, used in the signature.
true for the PayFast sandbox.
PayFast success return URL.
PayFast cancel return URL.
PayFast ITN notify URL.
Paysafe API key.
Paysafe API secret.
Paysafe account ID.
true for the Paysafe test environment.
Paysafe return URL.
Paysafe webhook target.
Paytm merchant ID.
Paytm merchant key.
Paytm website value, e.g. WEBSTAGING in test.
Paytm industry type ID.
true for the Paytm staging environment.
Paytm callback URL.
Paytm webhook target.
PayU merchant ID.
PayU merchant key.
PayU merchant salt, used in the request hash.
true for the PayU test environment.
Path appended to FRONTEND_URL for a successful PayU payment.
Path appended to FRONTEND_URL for a failed PayU payment.
Path appended to FRONTEND_URL when the customer cancels.
PayU callback path.
PayU webhook target.

Both gateways build their return URLs as ${FRONTEND_URL}${path}. With FRONTEND_URL unset the customer is sent to the literal string undefined/finance/deposit?status=success&ref=… and never returns to the site. Add FRONTEND_URL to .env before enabling either gateway.

Forex A-book execution

Hedge-execution venue credentials for the forex trading extension's A-book layer. These are not the market-data provider keys. All optional — leave blank for pure B-book operation.

OANDA API key.
MetaApi access token.

AI services

Google Gemini API key. Used by the AI KYC verification path and the health check.
DeepSeek API key, the alternative AI verification provider.
Declared in the template but read by nothing — see the dead-config list below.

Blockchain and the ecosystem extension

.env.example declares no RPC endpoint for any chain. A comment block describes the naming convention and stops there, so every endpoint the ecosystem extension needs has to be added by hand. Two things are exceptions. The explorer and transaction-provider keys further down are in the template, under Explorer / transaction-history providers. And custom EVM chains live in the ecosystem_custom_chain table, are managed from Admin → Ecosystem → Custom EVM Chains, and are written into process.env at boot from the database.

The naming convention is mechanical:

ETH_NETWORK="mainnet"
ETH_MAINNET_RPC="https://..."
ETH_MAINNET_RPC_WSS="wss://..."
ETH_EXPLORER_API_KEY="..."

<SYMBOL>_NETWORK selects the network (default mainnet), and the code then reads <SYMBOL>_<NETWORK>_RPC and <SYMBOL>_<NETWORK>_RPC_WSS for that network. <SYMBOL>_EXPLORER_API_KEY is the per-chain Etherscan key, tried before ETHERSCAN_API_KEY rather than instead of it — the two lists are concatenated, so a stale per-chain key no longer shadows a working global one. The EVM symbols in use are ETH, BSC, POLYGON, FTM, OPTIMISM, ARBITRUM, CELO, BASE, RSK, plus MO.

UTXO chains take node connection details instead: <SYMBOL>_NODE_HOST (default 127.0.0.1), _NODE_PORT, _NODE_USER, _NODE_PASSWORD, and <SYMBOL>_MEMPOOL_API_URL. Non-EVM chains use their own families — TRON_NETWORK and TRON_API_KEY, SOL_NETWORK and SOLANA_RPC_URL, TON_NETWORK with TON_MAINNET_RPC and TON_MAINNET_RPC_API_KEY, XMR_DAEMON_RPC_URL (default http://127.0.0.1:18081/json_rpc) and XMR_WALLET_RPC_URL (default port 18083).

Bitcoin network selector.
Which Bitcoin data source the deposit scanner uses.
BlockCypher token, used by the UTXO provider when BlockCypher is the selected node.
Etherscan API V2 multichain key, used for any EVM chain with no <SYMBOL>_EXPLORER_API_KEY of its own. Without it, transaction history, token metadata and contract verification lookups fall through to the keyless providers described below — which cover most chains, but not BSC, Fantom, Cronos, HECO or Polygon Amoy.
Set to true to run the ecosystem deposit monitors. Off by default.

ARBIRUM_MAINNET_RPC and ARBIRUM_MAINNET_RPC_WSS — missing the second "T" — are still read as fallbacks by the admin balance endpoint and the system health check. The real provider path only reads the correctly spelled ARBITRUM_MAINNET_RPC.

Set only the typo key and you get the worst outcome available: health reports Arbitrum as Up while deposits and withdrawals are broken. Neither spelling is in .env.example. Always set ARBITRUM_MAINNET_RPC.

Explorer and transaction-history providers

Seven providers serve EVM transaction history and native-deposit detection, tried in a per-chain order with automatic failover. Two of them — Blockscout and Routescan — need no credential, and each is appended to the end of the order of every chain it can serve, so most chains work with none of these keys set. Five chains are the exception: BSC (56 and 97), Fantom (250 and 4002), Cronos (25), HECO (128 and 256) and Polygon Amoy (80002) have neither a hosted Blockscout instance nor Routescan coverage, so none of them gets a keyless provider appended to its order. BSC is the one where a keyed provider is the normal answer for a production install: NODEREAL_API_KEY is free for BSC mainnet, and on BSC testnet only MORALIS_API_KEY / COVALENT_API_KEY index it.

Every provider key, ETHERSCAN_API_KEY above included, may hold several comma-separated keys, rotated through on auth, plan and rate-limit failures, and every one has a chain-scoped form — BSC_NODEREAL_API_KEY, POLYGON_COVALENT_API_KEY — that is tried first with the global value behind it as a spare. The full per-chain picture is in the Ecosystem environment reference.

Ankr Advanced API key. Covers most mainnets on a free tier; does not index BSC testnet.
Moralis key. Covers the major EVM chains and their testnets.
Covalent / GoldRush bearer token. One of the two providers that index BSC testnet — Ankr and NodeReal do not.
NodeReal key. ETH and BSC mainnet only, and the free replacement for the retired BscScan API.
Optional. Blockscout serves without a key; this only lifts the anonymous per-IP rate limit.
Optional. Routescan serves without a key; this only lifts the anonymous per-IP rate limit.
Comma-separated provider order for every chain, overriding the built-in per-chain defaults. Unknown names are dropped with a warning.
Comma-separated provider order for one chain, e.g. TRANSACTION_PROVIDERS_BSC. Beats TRANSACTION_PROVIDERS and the built-in default.
Set to true to stop the keyless Blockscout/Routescan tail being appended to the order you configured.
Per-attempt provider timeout in milliseconds. Values below 1000 are ignored.
Records requested per provider call, 1-10000. Etherscan's free tier caps at 1,000; Moralis and Ankr cap a page at 100 regardless, and NodeReal at 100 — it asks for 50 incoming and 50 outgoing transfers in two calls, then merges and deduplicates them by hash.
Host of a self-hosted Blockscout instance for one chain, e.g. BSC_BLOCKSCOUT_HOST. Beats the built-in chain-id map.

Master wallet encryption

Two variables unlock every custodial private key on the install. Neither is in .env.example. Generate them once, before creating any master wallet:

node scripts/kms/generate.mjs

It creates a 32-byte key, asks for a passphrase of at least 12 characters, and writes the AES-256-GCM result back to .env as four colon-separated hex parts (IV, auth tag, ciphertext, salt).

The encrypted master encryption key. Four colon-separated hex parts.
The passphrase that decrypts it, at least 12 characters. Both are held in memory only for the life of the process.

Change or lose either value and every encrypted wallet on the install becomes permanently unreadable. There is no recovery path and no support workaround. pnpm env-manager refuses to edit them for exactly this reason. Back up .env somewhere the database backup does not live.

ScyllaDB

Ecosystem and futures order books, candles and trade tape live in ScyllaDB, not MySQL. The installer does not install it and .env.example declares none of these — the defaults below are what the code assumes when the variables are absent. Neither the built-in database backup nor mysqldump covers this data.

Comma-separated contact points. Defaults to a local node.
Local datacenter name, as Scylla reports it.
Scylla username, if the cluster requires authentication.
Scylla password.
Keyspace holding ecosystem orders, candles, order book, trades and stop orders.
Keyspace holding futures orders, positions, order book and candles.
Connections per host in the local datacenter pool.
Set to false to disable Scylla entirely. Ecosystem trading then answers 503 rather than failing at boot, which is the right shape for an install that does not use the ecosystem extension.

Licensing and product identity

The core product ID used for license checks and heartbeats. Leave it alone unless support tells you otherwise.
Signing secret for the machine-bound .lic files under lic/. Undeclared in the template, and two code paths disagree about what happens when it is unset — one falls back to a build-time constant, the other to the literal string default-secret. Set it explicitly or leave it entirely unset; do not set it on one install and not another.
How often the license heartbeat runs, in milliseconds. Clamped to between 5 minutes and 1 hour. Egress to updates.mashdiv.com must not be firewalled; there is a 72-hour grace period when it is unreachable.

Two-factor policy

The five withdrawTwoFactor* platform settings in Admin → System → Settings are the live controls. These three are the legacy fallbacks the login paths still read, and they are undeclared in .env.example.

Legacy fallback enabling email one-time codes at login and on withdrawals.
Legacy fallback enabling SMS one-time codes.
Legacy fallback enabling authenticator-app codes on withdrawals.

Other operational variables

None of these are in .env.example either, but several change behaviour you can observe.

Base URL for payment-gateway return paths. See the PayU warning above.
Sumsub API key, for the Sumsub KYC verification service.
Sumsub API secret.
Set to true, 1, yes or on to suppress all outbound mail. Useful on a staging clone of production data.
From address used by the notification service's email providers, distinct from the transport sender.
SMTP auth user when it differs from the sender address. Falls back to APP_NODEMAILER_SMTP_SENDER.
LEGACY. The Hummingbot addon now uses the platform's own client-IP resolver, so this needs no value on a new install — a proxy on this machine is trusted automatically. Kept as an alias so an existing install that set it keeps working; TRUST_PROXY wins when both are present.
Minimum log level. debug also turns on verbose API request logging.
How long a request may take before the log prints a second line naming where its time went — the four slowest steps of that request, with their durations. Nothing is printed below the threshold, so a healthy install is silent. Set it to 0 to turn the report off. Use it when an operation is reported as slow and you need to know which step to look at: it names the wallet hold, the book read or the matching handoff rather than leaving you with one total.
How long ecosystem order-book changes are gathered before ONE websocket frame is read and sent, per market. Placements, cancellations and fills ask for a frame rather than reading the book themselves, so a burst of bot quotes costs one read instead of one per event; the frame that goes out is read after the burst and is therefore fresher than any it replaced. 0 sends every change immediately, which is the older behaviour and is measurably more expensive on a market that has bots quoting it. The market data socket also re-sends a full book every two seconds regardless.
Google Translate API key, surfaced in the system health check.
Absolute path overriding where downloadable e-commerce product files are stored.
Absolute path overriding where P2P dispute attachments are stored.
Where the NFT blockchain backup service writes. Defaults to backups/nft under the project root.
Encryption key for those backups. Empty means unencrypted.
Inert. Read once into a constant in the binary cancel-order route and never used from there; cancellation refunds are priced from the Cancellation tab of binary settings. The 87 is the code's fallback, not a shipped entry.
Batch size the ecosystem deposit monitor requests per API poll.

Variables the code reads that the template never declares

Roughly two hundred variable names are read somewhere in backend/src and appear nowhere in .env.example. Most are tuning knobs with sane defaults. These are the ones that change whether something works:

ENCRYPTED_ENCRYPTION_KEY, ENCRYPTION_KEY_PASSPHRASE — every custodial private key on the install. Unrecoverable if lost.

FRONTEND_URL — PayU and Authorize.Net build customer return URLs from it. Unset produces undefined/finance/deposit?....

ARBITRUM_MAINNET_RPC — the correctly spelled key. The typo variant is read as a fallback by health checks only.

Every blockchain RPC endpoint: roughly 60 names across the <SYMBOL>_NETWORK / <SYMBOL>_<NET>_RPC families plus the UTXO node and non-EVM families described above.

All eight SCYLLA_* variables. Ecosystem and futures trading do not work without a reachable cluster, and no backup in the product covers its data.

REDIS_DB — the logical database index. Declared readers exist; the template stops at host, port and password.

TRUST_PROXY and HB_TRUST_PROXY — two independent proxy-trust flags, both required behind a reverse proxy.

NEXT_PUBLIC_2FA_EMAIL_STATUS, NEXT_PUBLIC_2FA_SMS_STATUS, NEXT_PUBLIC_2FA_APP_STATUS — read by every login path and the withdrawal 2FA resolver.

SUMSUB_API_KEY, SUMSUB_API_SECRET — the Sumsub KYC integration.

LICENSE_SECRET, MAIN_PRODUCT_ID, HEARTBEAT_INTERVAL.

APP_NODEMAILER_SMTP_USERNAME, APP_EMAIL_FROM, APP_EMAIL_FROM_NAME, APP_NODEMAILER_ALLOW_INSECURE_TLS, the three APP_NODEMAILER_DKIM_* variables, and MAIL_DISABLED.

A third set of names for values you have already configured, each read by exactly one file. EMAIL_PROVIDER, EMAIL_FROM, SENDGRID_API_KEY, SMTP_HOST and SMTP_PORT are read only by the admin notification-settings screen; SITE_NAME and SITE_DESCRIPTION only by the API docs generator; APP_DEFAULT_LOCALE only by the payment gateway extension.

Setting them does not configure mail or the site name. Use the APP_* and NEXT_PUBLIC_* names documented above.

RATE_LIMIT_EXPIRY is honoured as a fallback for RATE_LIMIT_EXPIRE. For years the code read one spelling and the template shipped the other, so the window was permanently 60 seconds and editing the documented variable changed nothing. Both work now; prefer RATE_LIMIT_EXPIRE.

Variables in the template that nothing reads

Setting any of these has no effect anywhere in the product. They are listed so you stop trying.

Variable Note
OPENAI_API_KEY The AI verification path supports Gemini and DeepSeek only. No OpenAI SDK is imported anywhere in the backend.
APP_CLIENT_PLATFORM Twenty lines of instructions in the template for a value with no reader.
APP_SUPPORT_PHONE_NUMBER No reader.
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID No reader. The only gtag reference in the repo is a TypeScript declaration.
NEXT_PUBLIC_FACEBOOK_PIXEL_ID No reader.
NEXT_PUBLIC_FRONTEND No reader.
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY Commented out. Consumed by Next.js internals if uncommented, never by application code.

The matching googleAnalyticsStatus and facebookPixelStatus switches in Admin → System → Settings → Integrations are equally inert — turning them on injects nothing.