You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document is the authoritative reference for every environment variable read by the
application. Variables are grouped by concern. For each variable the table shows:
Type — string | number | boolean | enum
Default — value used when the variable is absent (empty string means no default)
Required? — yes = server refuses to start without it; no = optional
Effect — what the variable controls
Tip: Copy .env.example to .env and fill in the required variables before
starting the server locally.
Comma-separated list of trusted proxy IPs/CIDRs. Passed to Express trust proxy. Set to the address of your load balancer in production
INSTANCE_ID
string
hostname
no
Unique identifier for this process instance. Appears in structured log output for distributed tracing
SHUTDOWN_TIMEOUT
number
30000
no
Alias for SHUTDOWN_TIMEOUT_MS (milliseconds). Time allowed for in-flight requests to drain before force-exit
SHUTDOWN_TIMEOUT_MS
number
30000
no
Milliseconds to wait for graceful shutdown before force-exit
REQUEST_TIMEOUT_MS
number
30000
no
Global per-request timeout in milliseconds. Streaming endpoints are exempt
2. Authentication & API Keys
Variable
Type
Default
Required?
Effect
API_KEYS
string
—
yes
Comma-separated list of raw API keys accepted by the legacy authentication path. At least one key is required
JWT_SECRET
string
—
no
Secret used to sign and verify JWT tokens. Must satisfy secret strength rules if set
HOME_DOMAIN
string
—
no
Stellar home domain used in SEP-10 web-auth challenges
REQUIRE_ADMIN_2FA
boolean
false
no
When true, admin endpoints require a valid TOTP code alongside the API key
TOTP_ISSUER
string
StellarDonationAPI
no
Issuer label embedded in TOTP QR codes
TOTP_WINDOW
number
1
no
Number of 30-second TOTP windows (±) accepted to tolerate clock skew
SEP10_CHALLENGE_TTL
number
300
no
Seconds a SEP-10 challenge token remains valid
AUTH_MAX_ATTEMPTS
number
5
no
Maximum failed authentication attempts before an IP is locked out
AUTH_WINDOW_MS
number
60000
no
Rolling window (ms) in which AUTH_MAX_ATTEMPTS failures trigger lockout
AUTH_LOCKOUT_MS
number
900000
no
Duration (ms) of the authentication lockout (default 15 min)
REQUIRE_REQUEST_SIGNING
boolean
false
no
When true, every mutating request must carry a valid HMAC request signature
REQUEST_SIGNING_SECRET
string
—
no
HMAC secret used to verify inbound request signatures
REQUEST_SIGNING_WINDOW_SECONDS
number
300
no
Seconds of clock skew tolerated in signed requests
REQUIRE_IDEMPOTENCY_KEY
boolean
false
no
When true, POST/PATCH/PUT requests without X-Idempotency-Key are rejected
SIGNED_URL_EXPIRY_MS
number
3600000
no
Lifetime of signed download/export URLs in milliseconds (default 1 h)
3. Encryption & Secrets
Variable
Type
Default
Required?
Effect
ENCRYPTION_KEY
string (64 hex)
—
yes
AES-256 key used to encrypt wallet secret keys and other sensitive data at rest. Generate with npm run generate-key. Changing this key makes all previously encrypted data unrecoverable.
ENCRYPTION_KEY_1
string (64 hex)
—
no
Previous encryption key used during key rotation. Required when ENCRYPTION_KEY_VERSION=1
ENCRYPTION_KEY_VERSION
number
0
no
Active key version index. Set to 1 during key rotation to indicate ENCRYPTION_KEY_1 is the current key
NEW_ENCRYPTION_KEY
string (64 hex)
—
no
Target key during a live re-encryption pass (npm run migrate:reencrypt)
ENCRYPTION_SECRET
string
—
no
Legacy symmetric encryption secret for non-wallet data paths
ENCRYPTION_PRIVATE_KEY
string
—
no
RSA/EC private key PEM used by asymmetric signing paths
ENCRYPTION_PUBLIC_KEY
string
—
no
RSA/EC public key PEM used to verify asymmetric signatures
EXPORT_SIGNING_SECRET
string
—
no
HMAC secret for signing CSV/JSON export files. Must be distinct from ENCRYPTION_KEY
ANONYMOUS_DONATION_SECRET
string
—
no
Secret from which anonymous donor tokens are derived. Must be distinct from ENCRYPTION_KEY
SIGNING_PROVIDER
enum
local
no
Signing backend: local (in-process), hsm, or kms. See Signing Providers
CoinGecko API key (CG-… format) for XLM/fiat exchange rate lookups. Without this, the unauthenticated endpoint is used (stricter rate limits)
FEDERATION_RECORDS
string
—
no
JSON-encoded static federation records for local development, bypassing live federation lookups
FEDERATION_DOMAIN
string
—
no
Domain used for Stellar federation lookups
API_BASE_URL
string
—
no
Publicly accessible base URL of this API, used in generated links (e.g. in receipts, webhooks)
CSP_REPORT_URI
string
—
no
URI to which CSP violation reports are sent
CSP_REPORT_ONLY
boolean
false
no
When true, CSP violations are reported but not enforced
COMPRESSION_LEVEL
number
6
no
zlib compression level (1–9) for gzip response encoding
COMPRESSION_THRESHOLD_BYTES
number
1024
no
Minimum response size (bytes) before compression is applied
WEBHOOK_ALLOW_TLS_SKIP_VERIFY
boolean
false
no
Disable TLS certificate verification for outbound webhook deliveries. Never use in production
22. Unsafe Development Flags
The following variables are provided for local development only. The startup checks
(src/utils/startupChecks.js) will abort the process if any is true when
NODE_ENV=production.
Variable
Purpose
Production behaviour
DISABLE_RATE_LIMIT
Bypass all rate-limiting middleware
Startup fails
CORS_ALLOW_ALL
Allow every origin in CORS responses
Startup fails
DEBUG_MODE
Enable verbose debug logging
Startup fails
DRY_RUN
Skip real Stellar transactions
Startup fails
In non-production environments the server starts but logs a prominent ⚠ WARN for each
active flag.
All signing and encryption secrets are validated at startup. The server will refuse to
start if any secret fails the following rules:
Minimum length — at least 32 bytes (64 hex chars for hex secrets).
No known placeholders — values containing changeme, secret, password,
placeholder, example, todo, fixme, or the patterns from .env.example are
rejected.
Unique across roles — ENCRYPTION_KEY, EXPORT_SIGNING_SECRET,
ANONYMOUS_DONATION_SECRET, and JWT_SECRET must all be distinct values.
Variable
Role
ENCRYPTION_KEY
AES-256 data encryption (64 hex chars)
EXPORT_SIGNING_SECRET
HMAC signature for CSV/JSON exports
ANONYMOUS_DONATION_SECRET
Token derivation for anonymous donations
JWT_SECRET
JWT signing
Generate strong secrets with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"# or
npm run generate-key
24. Startup Configuration Validation (#1234)
npm start runs npm run validate-env (and the boot sequence in src/app.js runs the
same checks again with exitOnFailure) before the HTTP port is bound. Invalid or missing
required configuration aborts startup with a non-zero exit code and a message naming
the offending variable; misconfigured optional configuration logs a ⚠ WARN and starts
with defaults.
Hard failures (startup aborts)
Variable / condition
Expectation
ENCRYPTION_KEY
exactly 64 hexadecimal characters, not a placeholder in production
API_KEYS
at least one non-empty key
HORIZON_URL (when set)
a valid http(s) URL; HTTPS required in production
DB_PATH (effective)
parent directory exists and is writable; file readable/writable when present
Missing Stellar signing keys in production on a live network
Horizon tuning knobs out of range (HORIZON_POOL_SIZE, timeouts, retry/circuit-breaker settings)
Incoherent pool ranges (DB_POOL_MIN > DB_POOL_MAX)
MOCK_STELLAR=true in production
STELLAR_ENVIRONMENT and STELLAR_NETWORK set to different values
Secrets absent in production (EXPORT_SIGNING_SECRET, ANONYMOUS_DONATION_SECRET, JWT_SECRET)
25. SSRF Protection
All outbound HTTP requests (webhooks, IPFS pinning, federation lookups) are validated by
src/utils/ssrf.js before the connection is made. The validator:
Enforces HTTPS only (rejects http:, file:, etc.).
Blocks requests to private/loopback/link-local/cloud-metadata IP ranges:
StellarService round-robins Horizon calls across a small pool of Horizon.Server
instances (src/services/HorizonPool.js) so that a single misbehaving connection
doesn't serialize every Stellar call behind it. Pool behaviour is controlled by:
Variable
Default
Purpose
HORIZON_POOL_SIZE
3 (capped at 10)
Number of Horizon.Server instances per process
HORIZON_POOL_COOLDOWN_MS
30000
How long a member that hit a transient network error stays out of rotation before a health-check re-admits it
Why pool size matters
Too small — every retry/backoff on a failing member serializes calls onto
the remaining members, increasing p95/p99 latency under load and bringing
the failing member back into rotation (via cooldown) before traffic has
recovered.
Too large — HORIZON_POOL_SIZE is per process. The number that matters
for rate-limit purposes is HORIZON_POOL_SIZE × number_of_instances. The
public Horizon fleet (horizon.stellar.org / horizon-testnet.stellar.org)
rate-limits per source IP; a self-hosted Horizon enforces whatever limit
operators configure. Sizing the pool without accounting for instance count
is the single most common way to get an entire fleet throttled at once.
Sizing guidance
Start from your Horizon rate limit (requests/second) for the IP(s) your
fleet egresses from.
Decide your target fleet-wide concurrent-request budget, leaving headroom
(e.g. 70-80% of the hard limit) for retries and bursts.
HORIZON_POOL_SIZE ≈ target_budget / number_of_instances. Round down —
it is always safer to under-provision a pool (callers wait briefly via
round-robin reuse) than to over-provision and trip the shared rate limit.
Re-check the math whenever you change instance count (e.g. autoscaling)
— the pool size is per-instance and does not adjust itself.
HORIZON_POOL_COOLDOWN_MS should be long enough that a transient Horizon
blip doesn't flap a member in and out of rotation, but short enough that a
recovered Horizon node isn't left idle for minutes. 30s (the default) is a
reasonable starting point; raise it if horizon_pool_cooldown_events_total
shows members flapping repeatedly.
Observability
Pool health is exposed via Prometheus metrics (src/utils/metrics.js,
scraped at /metrics):
Metric
Type
Meaning
horizon_pool_size
Gauge
Configured pool size
horizon_pool_healthy_count
Gauge
Members currently in rotation
horizon_pool_unhealthy_count
Gauge
Members currently cooling down
horizon_pool_cooldown_events_total
Counter
Times a member was marked unhealthy after a transient failure
horizon_pool_recovery_events_total
Counter
Times a member was re-admitted after cooldown
horizon_pool_acquire_duration_seconds
Histogram
Time spent in getServer() acquiring a pool member
A sustained rise in horizon_pool_unhealthy_count or a high rate of
horizon_pool_cooldown_events_total indicates the pool is undersized or
Horizon itself is degraded — both are strong signals to revisit the sizing
math above before tripping the fleet-wide rate limit.
Retry / circuit-breaker integration
Cooldown is already tied into the unified retry policy: StellarService._executeWithRetry
(src/services/StellarService.js) wraps every Horizon call in the shared
circuit breaker, and on a transient network error it calls
HorizonPool.markUnhealthy() for the specific member that failed before
retrying on the next pool member. This means a single bad connection is
isolated within the same retry attempt rather than waiting for a future
request to discover it.