Environment Variables
Every environment variable the backend reads — what it controls, its default/fallback, where it's read, and PROD vs SANDBOX differences. Plus the insecure fallbacks, drift risks, and dead vars.
Every env var the Go backend reads, grouped by subsystem. Central helper utils.FetchEnv(name, fallback) (utils/utils.go:53) returns the value or the fallback — never fatal on its own; feature flags use parseBoolEnv (utils/feature_flags.go:239). There is no viper/envconfig and no struct env:"" tags — every read is an explicit call. Deploy env lives in /etc/octopus/octopus.env (prod) and a separate sandbox file, loaded via set -a && . …env.
The three things to check first
JWT_SECREThas an insecure hardcoded fallback"your-super-secret-key"— if unset, every JWT is signed with a public constant. 2.APP_KEYfails closed (fatal if unset) — the one that's done right. 3. A large set of vars are read in code but never set by deploy (email, prod queue/cache-prefix, forex, link base URLs) — see Drift risks.
Core / app
| Var | Controls | Default | Read at | PROD / SANDBOX |
|---|---|---|---|---|
APP_ENV | Selects prod vs dev branches for cookie Secure/Domain, portal CORS origins, isProduction() guards | "" (→ non-prod: insecure cookies, localhost origins) | config/cookie.go:31,52, web/domains.go:68 | production / sandbox. isProduction() only matches production/prod — sandbox is NOT "production" |
APP_NAME | TOTP issuer, WebAuthn RP display-name fallback | "Octopus" | services/totp.go:37,45, services/webauthn.go:35 | operator-prompted |
PORT | Fiber listen address | ":8081" | main.go:521 | :8081 / ${SANDBOX_PORT} (:8082) |
SERVICE_NAME | OTel service name + metric prefix + log field | "octopus" | main.go:128, metrics/instruments.go:16 | octopus / octopus-sandbox |
ENVIRONMENT (fallback ENV) | OTel deployment.environment resource attr | "local" | main.go:129 | production / sandbox |
SERVICE_VERSION | OTel service.version | "1.0.0" | utils/log_exporter.go:61 | not set by deploy |
LOG_LEVEL | Zap atomic level | "info" | singleton/singleton.go:69 | not set by deploy |
JOB_LOGS_DIR | Base dir for job-execution logs | /var/log/octopus/job_executions | jobs/execution_log_manager.go:50 | sandbox sets it (strict-protect) |
GO_ENV | Dead — written by deploy, never read in Go (code uses APP_ENV) | — | — | production / sandbox |
Database (Postgres)
| Var | Controls | Default | Read at |
|---|---|---|---|
DATABASE | Driver: postgres vs sqlite | postgres | database/database.go:30 |
PG_HOST / PG_PORT | Primary host/port | localhost / 5432 | database/database.go:40-41 |
PG_USER / PG_PASS | User / password (secret) | octopus / octopus (insecure fallback) | database/database.go:42-43 |
PG_DB / PG_SSLMODE | DB name / sslmode | octopus / disable | database/database.go:44-45 |
PG_MAX_OPEN_CONNS / PG_MAX_IDLE_CONNS | Pool sizes | 100 / 25 | database/database.go:46-47 |
PG_CONN_MAX_LIFETIME | Conn lifetime (s) | 300 | database/database.go:48 |
PG_READ_* | Read-replica DSN (each falls back to primary) | primary value | database/database.go:55-60 |
PG_TENANT_CONFIG | Path to multi-tenant conn-string config | "" | database/database.go:98 |
SQLITE_DB | SQLite path (when DATABASE=sqlite) | ./default.db | database/database.go:115 |
DB_QUERY_LOGGING | Toggle SQL logging | false | database/postgres.go:25 |
Deploy sets host/port/db/user/pass/sslmode. Prod PG_PASS is auto-generated (openssl rand -base64 24); sandbox reuses the shared container password with DB octopus_sandbox. Pool sizes, read-replica, tenant-config are not set by deploy.
Cache (Valkey)
| Var | Controls | Default | Read at |
|---|---|---|---|
CACHE_TYPE | memory vs valkey | memory | cache/cache.go:323 |
CACHE_PREFIX | Key namespace (isolates sandbox on shared Valkey) | "" | cache/cache.go:324 |
VALKEY_ADDR | host:port | localhost:6379 | cache/cache.go:332 |
VALKEY_PASS | password (secret) | "" | cache/cache.go:333 |
Prod sets CACHE_TYPE=valkey + VALKEY_ADDR but not CACHE_PREFIX; sandbox sets CACHE_PREFIX to namespace the shared instance.
Queue (RabbitMQ)
| Var | Controls | Default | Read at |
|---|---|---|---|
QUEUE_PROVIDER | Backend type | rabbitmq | queue/queue.go:143 |
QUEUE_CONNECTION | AMQP URL (secret — embeds creds) | amqp://octopus:octopus@localhost:5672/ (insecure fallback) | queue/queue.go:144 |
QUEUE_EXCHANGE | Exchange name | app.default | queue/queue.go:92 |
Prod sets no QUEUE_* vars (relies on defaults); sandbox sets all three with QUEUE_EXCHANGE=app.sandbox to isolate messages. Note the queue is mostly dormant — background work is cron-driven.
Auth / crypto
| Var | Controls | Default | Sensitivity |
|---|---|---|---|
JWT_SECRET | HMAC key for all JWTs (admin, client cookie, G2A, seeder creds) | "your-super-secret-key" | SECRET — dangerous fallback (10 read sites incl. middleware/auth.go:23, g2a_auth.go:34, client_cookie_auth.go:26) |
APP_KEY | AES-256 key (SHA-256 derived) encrypting DB conn strings / vendor data | "" → fatal at init | SECRET — fails closed (utils/encryption.go:58) |
COOKIE_DOMAIN | Portal cookie Domain (prod) | "" | prod env: not set; sandbox sets it |
CLIENT_PORTAL_ORIGINS | Portal CORS origins (prod; dev hardcodes localhost) | "" | prod: not set; sandbox sets it (config/cookie.go:54) |
WEBAUTHN_RP_ID | Passkey Relying-Party ID | localhost | prod=domain |
WEBAUTHN_RP_NAME | Passkey RP display name | APP_NAME | — |
WEBAUTHN_ORIGINS | Allowed passkey origins (comma-sep) | localhost:3000,3001 | per-env |
Deploy generates distinct JWT_SECRET (rand -base64 64) and APP_KEY (rand -base64 32) per environment so tokens/ciphertext don't cross. See Auth & Access and Secrets & Config.
| Var | Controls | Default |
|---|---|---|
EMAIL_PROVIDER | sendgrid vs smtp | sendgrid |
SENDGRID_API_KEY | API key (secret) | "" |
EMAIL_DEFAULT_SENDER | From address | "" |
SMTP_HOST / SMTP_PORT | SMTP host / port | "" / 587 |
SMTP_USERNAME / SMTP_PASSWORD | SMTP creds (secret) | "" |
SMTP_SECURE_MODE / SMTP_INSECURE_CERT | TLS / skip-verify | false / false |
All in email/email.go:66-88.
No email vars are set in either prod or sandbox env files — email is effectively unconfigured unless added manually. Remember SMTP transport drops attachments, so sendgrid is the only fully-working provider.
Telemetry (OpenTelemetry)
| Var | Controls | Default |
|---|---|---|
OTEL_ENABLED | Master switch (fatal if unparseable) | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector host:port (traces/metrics/logs → SigNoz) | localhost:4318 |
OTEL_EXPORTER_OTLP_TOKEN | SigNoz access token (signoz-access-token header, secret) | "" |
INSECURE_MODE | OTLP WithInsecure() (no TLS) | true (insecure default) |
OTEL_METRIC_EXPORT_INTERVAL | Metrics interval (s) | 60 |
There is no SIGNOZ_ENDPOINT — SigNoz is reached via the OTLP endpoint/token. Reads in utils/{tracer,metrics_exporter,log_exporter}.go. See Infrastructure → telemetry and Jobs & Observability.
Feature flags
Populated once at startup by utils.InitFeatureFlags() (main.go:118 → feature_flags.go:42), read via parseBoolEnv (true only for true/1/yes, case-insensitive) — all default OFF. No DB or remote source.
| Var | Effect | Extra gating |
|---|---|---|
FEATURE_VOUCHERS_ENABLED | Vouchers vertical | prerequisite for Shopify + VoucherLinks |
FEATURE_PAYOUTS_ENABLED | Payouts & beneficiaries | + per-client ClientPayoutConfig.PayoutsEnabled |
FEATURE_SHOPIFY_ENABLED | Shopify sync | + vouchers ON + per-client ShopifyEnabled && ShopifySyncEnabled |
FEATURE_VOUCHER_LINKS_ENABLED | Voucher links | + vouchers ON + per-client IsLinkEnabled |
FEATURE_TOPUPS_ENABLED | Top-ups vertical | — |
FEATURE_ESIM_ENABLED | eSIM vertical | — |
Global getters feature_flags.go:59-88; per-client combination :96-170; surfaced to the portal via GetEnabledFeaturesForClient (/client/api/me) and to admin nav via GetGlobalEnabledFeatures. Deploy prompts the operator and writes all six. A disabled feature's routes are never registered (API Reference, Admin Panel).
Vendor / external
Most vendor creds live in the vendor_attributes DB table, not env — see Vendors. The env-based ones:
| Var | Controls | Default |
|---|---|---|
APP_BASE_URL | Self-loop guard — OCTO adapters refuse a host resolving to our own base URL (only if the vendor's self_base_url DB attr is empty) | "" |
GV_LINK_BASE_URL | Base URL for generated gift-voucher links | http://localhost |
VOUCHER_BASE_URL | Base URL for voucher order links | https://example.com |
XE_USERNAME / XE_PASSWORD | XE.com forex creds (secret; both required or forex errors) | "" |
None are set by any deploy script — forex will error, links fall back to example/localhost, and the self-loop guard relies solely on the DB self_base_url. See Octopus adapters and Wallet charges.
Test / local only
MOCKY_BASE_URL (test/testhelpers/mocky.go, default http://localhost:8788), OCTOPUS_SANDBOX_ALLOW_ORDER, CI/GITHUB_ACTIONS test gates. See Testing.
Frontend / worker (brief)
- Next.js client: build-time
NEXT_PUBLIC_{API_URL,ASSET_BASE_URL,BRAND,CLAIM_HOST,TURNSTILE_SITE_KEY,APP_NAME}. - Docs: mirrors
FEATURE_*_ENABLED(display-only) + white-labelAPP_*. - Grasshopper worker (
wrangler.jsoncvars):TURNSTILE_SITE_KEY,OTEL_*,OTEL_INGEST_TOKEN; sandboxenv.sandbox.varsaddsTURNSTILE_SECRET_KEY,OCTOPUS_API_URL,OCTOPUS_CLIENT_USERNAME/_PASSWORD,ENCRYPTION_KEY,ADMIN_TOKEN,CLAIM_NONCE_SECRET— several committed (see Known Issues + Secrets). Prod worker secrets go viawrangler secret put.
Insecure fallbacks
| Var | Fallback | Risk |
|---|---|---|
JWT_SECRET | "your-super-secret-key" (10 sites) | All JWTs signed with a public constant if unset |
PG_PASS | octopus | default DB password |
QUEUE_CONNECTION | amqp://octopus:octopus@… | embedded default creds |
INSECURE_MODE | true | OTLP without TLS by default |
APP_KEY | — (fatal) | ✅ fails closed — the correct pattern |
Drift risks & dead vars
Read in code but NOT set by deploy (rely on defaults)
SERVICE_VERSION, LOG_LEVEL, JOB_LOGS_DIR, OTEL_METRIC_EXPORT_INTERVAL, all PG_* pool/read-replica vars + PG_TENANT_CONFIG + SQLITE_DB, DB_QUERY_LOGGING, all email vars, all QUEUE_* in prod (sandbox sets them), CACHE_PREFIX in prod, COOKIE_DOMAIN / CLIENT_PORTAL_ORIGINS in prod, APP_BASE_URL, GV_LINK_BASE_URL, VOUCHER_BASE_URL, XE_USERNAME, XE_PASSWORD.
- Set by deploy but never read (dead):
GO_ENV— code usesAPP_ENV. - Set in local
.envbut never read in Go (dead — but real credentials):TWILLIO_ACCOUNT_ID,TWILLIO_AUTH_TOKEN,OPENAI_API_KEY,VECTOR_DB_HOST. These are the committed-secret leak — rotate regardless of being unused.
Key files
- Helper:
utils/utils.go:53(FetchEnv),utils/feature_flags.go(flags) - DB:
database/database.go; cache:cache/cache.go; queue:queue/queue.go - Auth:
utils/encryption.go,config/cookie.go,services/webauthn.go - Email:
email/email.go; telemetry:utils/{tracer,metrics_exporter,log_exporter}.go - Deploy env heredocs:
deploy/setup-octopus-service.sh:286,deploy/setup-octopus-sandbox-service.sh
Secrets & Configuration
Where every credential, secret, and config value lives across the platform — and how to rotate each one.
Deploy & Release
How to ship a change — the deploy/ scripts, systemd units, nginx vhosts, on-server Go build, Goose migrations in deploy, rollback, and a step-by-step sandbox→prod runbook.