OctoWiki

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

  1. JWT_SECRET has an insecure hardcoded fallback "your-super-secret-key" — if unset, every JWT is signed with a public constant. 2. APP_KEY fails 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

VarControlsDefaultRead atPROD / SANDBOX
APP_ENVSelects 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:68production / sandbox. isProduction() only matches production/prod — sandbox is NOT "production"
APP_NAMETOTP issuer, WebAuthn RP display-name fallback"Octopus"services/totp.go:37,45, services/webauthn.go:35operator-prompted
PORTFiber listen address":8081"main.go:521:8081 / ${SANDBOX_PORT} (:8082)
SERVICE_NAMEOTel service name + metric prefix + log field"octopus"main.go:128, metrics/instruments.go:16octopus / octopus-sandbox
ENVIRONMENT (fallback ENV)OTel deployment.environment resource attr"local"main.go:129production / sandbox
SERVICE_VERSIONOTel service.version"1.0.0"utils/log_exporter.go:61not set by deploy
LOG_LEVELZap atomic level"info"singleton/singleton.go:69not set by deploy
JOB_LOGS_DIRBase dir for job-execution logs/var/log/octopus/job_executionsjobs/execution_log_manager.go:50sandbox sets it (strict-protect)
GO_ENVDead — written by deploy, never read in Go (code uses APP_ENV)production / sandbox

Database (Postgres)

VarControlsDefaultRead at
DATABASEDriver: postgres vs sqlitepostgresdatabase/database.go:30
PG_HOST / PG_PORTPrimary host/portlocalhost / 5432database/database.go:40-41
PG_USER / PG_PASSUser / password (secret)octopus / octopus (insecure fallback)database/database.go:42-43
PG_DB / PG_SSLMODEDB name / sslmodeoctopus / disabledatabase/database.go:44-45
PG_MAX_OPEN_CONNS / PG_MAX_IDLE_CONNSPool sizes100 / 25database/database.go:46-47
PG_CONN_MAX_LIFETIMEConn lifetime (s)300database/database.go:48
PG_READ_*Read-replica DSN (each falls back to primary)primary valuedatabase/database.go:55-60
PG_TENANT_CONFIGPath to multi-tenant conn-string config""database/database.go:98
SQLITE_DBSQLite path (when DATABASE=sqlite)./default.dbdatabase/database.go:115
DB_QUERY_LOGGINGToggle SQL loggingfalsedatabase/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)

VarControlsDefaultRead at
CACHE_TYPEmemory vs valkeymemorycache/cache.go:323
CACHE_PREFIXKey namespace (isolates sandbox on shared Valkey)""cache/cache.go:324
VALKEY_ADDRhost:portlocalhost:6379cache/cache.go:332
VALKEY_PASSpassword (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)

VarControlsDefaultRead at
QUEUE_PROVIDERBackend typerabbitmqqueue/queue.go:143
QUEUE_CONNECTIONAMQP URL (secret — embeds creds)amqp://octopus:octopus@localhost:5672/ (insecure fallback)queue/queue.go:144
QUEUE_EXCHANGEExchange nameapp.defaultqueue/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

VarControlsDefaultSensitivity
JWT_SECRETHMAC 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_KEYAES-256 key (SHA-256 derived) encrypting DB conn strings / vendor data""fatal at initSECRET — fails closed (utils/encryption.go:58)
COOKIE_DOMAINPortal cookie Domain (prod)""prod env: not set; sandbox sets it
CLIENT_PORTAL_ORIGINSPortal CORS origins (prod; dev hardcodes localhost)""prod: not set; sandbox sets it (config/cookie.go:54)
WEBAUTHN_RP_IDPasskey Relying-Party IDlocalhostprod=domain
WEBAUTHN_RP_NAMEPasskey RP display nameAPP_NAME
WEBAUTHN_ORIGINSAllowed passkey origins (comma-sep)localhost:3000,3001per-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.

Email

VarControlsDefault
EMAIL_PROVIDERsendgrid vs smtpsendgrid
SENDGRID_API_KEYAPI key (secret)""
EMAIL_DEFAULT_SENDERFrom address""
SMTP_HOST / SMTP_PORTSMTP host / port"" / 587
SMTP_USERNAME / SMTP_PASSWORDSMTP creds (secret)""
SMTP_SECURE_MODE / SMTP_INSECURE_CERTTLS / skip-verifyfalse / 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)

VarControlsDefault
OTEL_ENABLEDMaster switch (fatal if unparseable)false
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector host:port (traces/metrics/logs → SigNoz)localhost:4318
OTEL_EXPORTER_OTLP_TOKENSigNoz access token (signoz-access-token header, secret)""
INSECURE_MODEOTLP WithInsecure() (no TLS)true (insecure default)
OTEL_METRIC_EXPORT_INTERVALMetrics 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:118feature_flags.go:42), read via parseBoolEnv (true only for true/1/yes, case-insensitive) — all default OFF. No DB or remote source.

VarEffectExtra gating
FEATURE_VOUCHERS_ENABLEDVouchers verticalprerequisite for Shopify + VoucherLinks
FEATURE_PAYOUTS_ENABLEDPayouts & beneficiaries+ per-client ClientPayoutConfig.PayoutsEnabled
FEATURE_SHOPIFY_ENABLEDShopify sync+ vouchers ON + per-client ShopifyEnabled && ShopifySyncEnabled
FEATURE_VOUCHER_LINKS_ENABLEDVoucher links+ vouchers ON + per-client IsLinkEnabled
FEATURE_TOPUPS_ENABLEDTop-ups vertical
FEATURE_ESIM_ENABLEDeSIM 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:

VarControlsDefault
APP_BASE_URLSelf-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_URLBase URL for generated gift-voucher linkshttp://localhost
VOUCHER_BASE_URLBase URL for voucher order linkshttps://example.com
XE_USERNAME / XE_PASSWORDXE.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-label APP_*.
  • Grasshopper worker (wrangler.jsonc vars): TURNSTILE_SITE_KEY, OTEL_*, OTEL_INGEST_TOKEN; sandbox env.sandbox.vars adds TURNSTILE_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 via wrangler secret put.

Insecure fallbacks

VarFallbackRisk
JWT_SECRET"your-super-secret-key" (10 sites)All JWTs signed with a public constant if unset
PG_PASSoctopusdefault DB password
QUEUE_CONNECTIONamqp://octopus:octopus@…embedded default creds
INSECURE_MODEtrueOTLP 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 uses APP_ENV.
  • Set in local .env but 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

On this page