Skip to main content

Configuration & Environment Variables

Central reference for configuring the Heimdall platform: the Rust API (and bots) load layered TOML files with environment-variable overrides via the heimdall-config crate, while the Next.js apps (backend, id, policies) are configured purely through .env files.

Config Resolution Order (API)

The API uses heimdall-config (crates/heimdall-config/src/lib.rs). Configuration is built from four sources, where later sources override earlier ones:

  1. config/default.toml — base defaults (required, load fails if missing)
  2. config/{APP_ENV}.toml — run-mode overrides, e.g. production.toml (optional)
  3. config/local.toml — local developer overrides, git-ignored (optional)
  4. HEIMDALL__* environment variables — runtime overrides (highest priority)

The run mode comes from the APP_ENV environment variable and defaults to "development" when unset. The API config lives in platform/api/config/ (default.toml, staging.toml, production.toml, local.toml).

// Settings::load() in heimdall-config
crate::load("APP_ENV", "HEIMDALL")

Environment-variable override convention

Verified from heimdall-config:

  • Prefix: HEIMDALL
  • Prefix separator: __ (double underscore)
  • Nesting separator: __ (double underscore)

So a TOML key maps to an env var by uppercasing the path and joining sections with __:

TOMLEnvironment variable
database.urlHEIMDALL__DATABASE__URL
server.portHEIMDALL__SERVER__PORT
email.smtp.hostHEIMDALL__EMAIL__SMTP__HOST
storage.upload_limits.images.max_size_mbHEIMDALL__STORAGE__UPLOAD_LIMITS__IMAGES__MAX_SIZE_MB

List-valued keys are parsed as comma-separated strings. The following keys are registered for list parsing (others stay scalar):

  • cors.allowed_origins
  • email.supported_locales
  • storage.upload_limits.images.allowed_types
  • storage.upload_limits.documents.allowed_types
  • storage.upload_limits.videos.allowed_types
  • storage.upload_limits.general.allowed_types

Example: HEIMDALL__CORS__ALLOWED_ORIGINS="https://a.com,https://b.com".

Code defaults vs. shipped default.toml

The Default column below is the compiled-in default from the Rust struct (#[serde(default = ...)]). The shipped platform/api/config/default.toml sometimes sets a different value (e.g. GraphQL path is "/gql" in default.toml but "/graphql" is the struct default). Where a field has no struct default it is required and must be supplied by default.toml (which is always loaded).


API Configuration Reference

All structs live in crates/heimdall-config/src/common.rs (top-level Settings).

[server]

KeyTypeDefaultDescriptionEnv override
hoststringrequired (127.0.0.1)Bind addressHEIMDALL__SERVER__HOST
portu16required (3000)Bind portHEIMDALL__SERVER__PORT
workersusize?none (4)Actix worker threadsHEIMDALL__SERVER__WORKERS
keep_aliveu6475Keep-alive timeout (seconds)HEIMDALL__SERVER__KEEP_ALIVE
public_urlstringhttp://localhost:3000Public base URL for OAuth redirects / external linksHEIMDALL__SERVER__PUBLIC_URL

[database] (PostgreSQL)

KeyTypeDefaultDescriptionEnv override
urlstringrequiredPostgreSQL connection URLHEIMDALL__DATABASE__URL
max_connectionsu32required (10)Max pool connectionsHEIMDALL__DATABASE__MAX_CONNECTIONS
min_connectionsu32required (2)Min idle connectionsHEIMDALL__DATABASE__MIN_CONNECTIONS
connect_timeoutu64required (30)Connect timeout (seconds)HEIMDALL__DATABASE__CONNECT_TIMEOUT
idle_timeoutu64required (600)Idle timeout (seconds)HEIMDALL__DATABASE__IDLE_TIMEOUT
max_lifetimeu641800Max connection lifetime (seconds)HEIMDALL__DATABASE__MAX_LIFETIME

[timescale] (TimescaleDB — GPS & audit time-series)

KeyTypeDefaultDescriptionEnv override
urlstringrequiredTimescaleDB connection URL (default port 5433)HEIMDALL__TIMESCALE__URL
max_connectionsu325Max pool connectionsHEIMDALL__TIMESCALE__MAX_CONNECTIONS
min_connectionsu321Min idle connectionsHEIMDALL__TIMESCALE__MIN_CONNECTIONS
connect_timeoutu6430Connect timeout (seconds)HEIMDALL__TIMESCALE__CONNECT_TIMEOUT
idle_timeoutu64600Idle timeout (seconds)HEIMDALL__TIMESCALE__IDLE_TIMEOUT
max_lifetimeu641800Max connection lifetime (seconds)HEIMDALL__TIMESCALE__MAX_LIFETIME
compression_after_hoursu32168Compress chunks older than N hours (7 days)HEIMDALL__TIMESCALE__COMPRESSION_AFTER_HOURS

[redis]

KeyTypeDefaultDescriptionEnv override
urlstringrequiredRedis connection URLHEIMDALL__REDIS__URL
pool_sizeusizerequired (10)Max pool connectionsHEIMDALL__REDIS__POOL_SIZE
timeoutu645Connection timeout (seconds)HEIMDALL__REDIS__TIMEOUT

[graphql]

KeyTypeDefaultDescriptionEnv override
playgroundbooltrueEnable GraphQL playgroundHEIMDALL__GRAPHQL__PLAYGROUND
pathstring/graphql (toml: /gql)GraphQL endpoint pathHEIMDALL__GRAPHQL__PATH
max_depthu3210Max query depthHEIMDALL__GRAPHQL__MAX_DEPTH
max_complexityu32100Max query complexityHEIMDALL__GRAPHQL__MAX_COMPLEXITY

[websocket]

KeyTypeDefaultDescriptionEnv override
pathstring/wsWebSocket endpoint pathHEIMDALL__WEBSOCKET__PATH
heartbeat_intervalu645Heartbeat interval (seconds)HEIMDALL__WEBSOCKET__HEARTBEAT_INTERVAL
client_timeoutu6410Client inactivity timeout (seconds)HEIMDALL__WEBSOCKET__CLIENT_TIMEOUT
max_message_sizeusize65536Max message size (bytes, 64 KB)HEIMDALL__WEBSOCKET__MAX_MESSAGE_SIZE
max_frame_sizeusize16384Max frame size (bytes, 16 KB)HEIMDALL__WEBSOCKET__MAX_FRAME_SIZE
max_channels_per_sessionusize100Max channels per WS sessionHEIMDALL__WEBSOCKET__MAX_CHANNELS_PER_SESSION

[auth]

KeyTypeDefaultDescriptionEnv override
jwt_secretstringrequired (empty)JWT signing secret — must be overriddenHEIMDALL__AUTH__JWT_SECRET
jwt_expirationi6486400Access token TTL (seconds, 24h)HEIMDALL__AUTH__JWT_EXPIRATION
refresh_token_expirationi642592000Refresh token TTL (seconds, 30d)HEIMDALL__AUTH__REFRESH_TOKEN_EXPIRATION
totp_encryption_keystring""Base64 AES-256 key for TOTP secrets (openssl rand -base64 32)HEIMDALL__AUTH__TOTP_ENCRYPTION_KEY

[cors]

KeyTypeDefaultDescriptionEnv override
allowed_originsstring[]requiredAllowed origins (comma-separated for env)HEIMDALL__CORS__ALLOWED_ORIGINS
allow_credentialsbooltrueAllow credentials in CORSHEIMDALL__CORS__ALLOW_CREDENTIALS
max_ageu643600Preflight cache max-age (seconds)HEIMDALL__CORS__MAX_AGE

[rate_limiting]

KeyTypeDefaultDescriptionEnv override
max_requestsu64100Max requests per windowHEIMDALL__RATE_LIMITING__MAX_REQUESTS
window_secsu6460Window length (seconds)HEIMDALL__RATE_LIMITING__WINDOW_SECS

[logging]

KeyTypeDefaultDescriptionEnv override
levelstringrequired (debug)Log level: trace/debug/info/warn/errorHEIMDALL__LOGGING__LEVEL
formatstringrequired (pretty)Log format: json or prettyHEIMDALL__LOGGING__FORMAT

[app]

KeyTypeDefaultDescriptionEnv override
environmentstringrequired (development)App environment: development/staging/productionHEIMDALL__APP__ENVIRONMENT
debugbooltrueDebug modeHEIMDALL__APP__DEBUG
namestringrequired (Heimdall API)Application nameHEIMDALL__APP__NAME

[apps]

Frontend URLs used for OAuth client redirect URIs on first launch.

KeyTypeDefaultDescriptionEnv override
backend_urlstringhttp://localhost:3001Backend/console app URLHEIMDALL__APPS__BACKEND_URL
id_urlstringhttp://localhost:3002ID (login) app URLHEIMDALL__APPS__ID_URL
policies_urlstringhttp://localhost:3004Policies app URLHEIMDALL__APPS__POLICIES_URL

[bots]

Optional bot health-check endpoints.

KeyTypeDefaultDescriptionEnv override
discord_bot_urlstring?noneDiscord bot health URL (e.g. http://localhost:3006/health)HEIMDALL__BOTS__DISCORD_BOT_URL
twitch_bot_urlstring?noneTwitch bot health URL (e.g. http://localhost:3007/health)HEIMDALL__BOTS__TWITCH_BOT_URL

[scheduler]

The whole section is optional (Option<SchedulerConfig>).

KeyTypeDefaultDescriptionEnv override
enabledbooltrueEnable background schedulerHEIMDALL__SCHEDULER__ENABLED
deletion_cronstring?0 */15 * * * *Account-deletion job cron (every 15 min)HEIMDALL__SCHEDULER__DELETION_CRON
integration_refresh_cronstring?0 */5 * * * *Integration token-refresh cron (every 5 min)HEIMDALL__SCHEDULER__INTEGRATION_REFRESH_CRON
integration_refresh_buffer_minutesi64?15Refresh this many minutes before token expiryHEIMDALL__SCHEDULER__INTEGRATION_REFRESH_BUFFER_MINUTES
channel_stats_refresh_cronstring?0 0 */6 * * *Channel-stats refresh cron (every 6h)HEIMDALL__SCHEDULER__CHANNEL_STATS_REFRESH_CRON
channel_stats_stale_minutesi64?360Minutes until channel stats are stale (6h)HEIMDALL__SCHEDULER__CHANNEL_STATS_STALE_MINUTES

Cron format is 6-field: sec min hour day month day-of-week. The defaults listed are the documented defaults; default.toml ships deletion_cron = "0 */1 * * * *".

[email]

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable email sendingHEIMDALL__EMAIL__ENABLED
providerstringconsolesendgrid, smtp, or consoleHEIMDALL__EMAIL__PROVIDER
from_emailstringnoreply@elcapitano.comSender addressHEIMDALL__EMAIL__FROM_EMAIL
from_namestringelcapitano IdentitySender display nameHEIMDALL__EMAIL__FROM_NAME
token_expiry_hoursu3224Verification-link TTL (hours)HEIMDALL__EMAIL__TOKEN_EXPIRY_HOURS
default_localestringenFallback email localeHEIMDALL__EMAIL__DEFAULT_LOCALE
supported_localesstring[]["en","de"]Supported email locales (comma-separated for env)HEIMDALL__EMAIL__SUPPORTED_LOCALES

[email.sendgrid]

KeyTypeDefaultDescriptionEnv override
api_keystring""SendGrid API keyHEIMDALL__EMAIL__SENDGRID__API_KEY

[email.smtp]

KeyTypeDefaultDescriptionEnv override
hoststringlocalhostSMTP hostHEIMDALL__EMAIL__SMTP__HOST
portu16587SMTP port (587 STARTTLS / 465 SSL / 25 plain)HEIMDALL__EMAIL__SMTP__PORT
usernamestring""SMTP usernameHEIMDALL__EMAIL__SMTP__USERNAME
passwordstring""SMTP passwordHEIMDALL__EMAIL__SMTP__PASSWORD
tlsbooltrueUse TLS/STARTTLSHEIMDALL__EMAIL__SMTP__TLS
timeoutu6430Connection timeout (seconds)HEIMDALL__EMAIL__SMTP__TIMEOUT

[turnstile]

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable Cloudflare Turnstile verificationHEIMDALL__TURNSTILE__ENABLED
secret_keystring""Turnstile server-side secret keyHEIMDALL__TURNSTILE__SECRET_KEY

[sentry]

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable Sentry error trackingHEIMDALL__SENTRY__ENABLED
dsnstring""Sentry DSNHEIMDALL__SENTRY__DSN
environmentstringdevelopmentSentry environment nameHEIMDALL__SENTRY__ENVIRONMENT
traces_sample_ratef321.0Transaction sample rate (0.0–1.0)HEIMDALL__SENTRY__TRACES_SAMPLE_RATE
sample_ratef321.0Error-event sample rate (0.0–1.0)HEIMDALL__SENTRY__SAMPLE_RATE
debugboolfalseSentry SDK debug modeHEIMDALL__SENTRY__DEBUG
attach_stacktracebooltrueAttach stack traces to messagesHEIMDALL__SENTRY__ATTACH_STACKTRACE
send_default_piiboolfalseSend PII in error reportsHEIMDALL__SENTRY__SEND_DEFAULT_PII
max_breadcrumbsusize100Max breadcrumbs capturedHEIMDALL__SENTRY__MAX_BREADCRUMBS

[geoip]

MaxMind GeoLite2 IP-to-location lookups used for audit enrichment.

KeyTypeDefaultDescriptionEnv override
database_pathstring?nonePath to GeoLite2 City .mmdbHEIMDALL__GEOIP__DATABASE_PATH
account_idstring?noneMaxMind account ID (Privacy Exclusions API)HEIMDALL__GEOIP__ACCOUNT_ID
license_keystring?noneMaxMind license key (auto-update / Privacy Exclusions)HEIMDALL__GEOIP__LICENSE_KEY
auto_updateboolfalseAuto-update DB on startup (needs license key)HEIMDALL__GEOIP__AUTO_UPDATE
update_interval_daysu327DB update interval (days)HEIMDALL__GEOIP__UPDATE_INTERVAL_DAYS
privacy_exclusions_enabledboolfalseEnable Privacy Exclusions APIHEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_ENABLED
privacy_exclusions_refresh_hoursu3224Refresh interval for exclusions (hours)HEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_REFRESH_HOURS
privacy_exclusions_cache_pathstringdata/geoip-privacy-exclusions.jsonLocal cache file pathHEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_CACHE_PATH

[integrations]

Streaming-platform OAuth (Twitch, YouTube, Kick, Trovo) — separate from user auth.

KeyTypeDefaultDescriptionEnv override
token_encryption_keystring""AES-256-GCM key for stored OAuth tokens (openssl rand -base64 32)HEIMDALL__INTEGRATIONS__TOKEN_ENCRYPTION_KEY
internal_service_keystring""Bot-to-API auth key (openssl rand -hex 32)HEIMDALL__INTEGRATIONS__INTERNAL_SERVICE_KEY

Each platform sub-section ([integrations.twitch], [integrations.youtube], [integrations.kick], [integrations.trovo]) has the same two keys:

KeyTypeDefaultDescriptionEnv override (example: twitch)
client_idstring""OAuth client IDHEIMDALL__INTEGRATIONS__TWITCH__CLIENT_ID
client_secretstring""OAuth client secretHEIMDALL__INTEGRATIONS__TWITCH__CLIENT_SECRET

Swap TWITCH for YOUTUBE, KICK, or TROVO for the other platforms.

[storage] (S3-compatible)

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable storage serviceHEIMDALL__STORAGE__ENABLED
endpointstringhttp://localhost:9000S3 endpoint URLHEIMDALL__STORAGE__ENDPOINT
regionstringus-east-1 (toml: auto)S3 regionHEIMDALL__STORAGE__REGION
access_keystring""S3 access key IDHEIMDALL__STORAGE__ACCESS_KEY
secret_keystring""S3 secret access keyHEIMDALL__STORAGE__SECRET_KEY
bucketstringheimdallDefault bucketHEIMDALL__STORAGE__BUCKET
path_styleboolfalseUse path-style URLs (required for MinIO)HEIMDALL__STORAGE__PATH_STYLE
public_urlstring?nonePublic CDN URL if different from endpointHEIMDALL__STORAGE__PUBLIC_URL
presigned_expiry_secondsu323600Presigned URL TTL (seconds)HEIMDALL__STORAGE__PRESIGNED_EXPIRY_SECONDS

[storage.upload_limits.*]

Four categories — images, documents, videos, general — each with the same keys:

KeyTypeDefault (images / documents / videos / general)Description
max_size_mbu645 / 25 / 500 / 100Max upload size (MB)
allowed_typesstring[]jpeg,png,webp,gif / pdf,plain,json / mp4,webm,quicktime,x-msvideo,x-matroska / [] (all)Allowed MIME types

Env overrides follow the nested rule, e.g. HEIMDALL__STORAGE__UPLOAD_LIMITS__IMAGES__MAX_SIZE_MB and the comma-separated HEIMDALL__STORAGE__UPLOAD_LIMITS__IMAGES__ALLOWED_TYPES.

[pegelonline] (WSV water-level monitoring)

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable Pegelonline pollingHEIMDALL__PEGELONLINE__ENABLED
base_urlstringhttps://pegelonline.wsv.de/webservices/rest-api/v2Pegelonline REST API baseHEIMDALL__PEGELONLINE__BASE_URL
radius_kmf6430.0Search radius around GPS position (km)HEIMDALL__PEGELONLINE__RADIUS_KM
stationsstring[][]Station whitelist (exact API shortnames); empty = radius-basedHEIMDALL__PEGELONLINE__STATIONS

[ais] (vessel tracking)

KeyTypeDefaultDescriptionEnv override
enabledboolfalseEnable AIS trackingHEIMDALL__AIS__ENABLED
api_keystring""AIS data-provider API keyHEIMDALL__AIS__API_KEY
radius_kmf6410.0Search radius around GPS position (km)HEIMDALL__AIS__RADIUS_KM
ignore_mmsiu32[][]MMSI numbers to exclude (e.g. own vessel)HEIMDALL__AIS__IGNORE_MMSI

Next.js App Environment Variables

The web apps (platform/backend, platform/id, platform/policies) are configured entirely via .env (see each app's .env.example). NEXT_PUBLIC_* vars are exposed to the browser; all others are server-only.

Common to all three apps

VariableExampleDescription
NEXTAUTH_URLhttp://localhost:3001NextAuth base URL (per app port)
NEXTAUTH_SECRETNextAuth session secret
HEIMDALL_CLIENT_IDheimdall-backend-clientOAuth client ID for this app
HEIMDALL_CLIENT_SECRETOAuth client secret
NEXT_PUBLIC_API_URLhttp://localhost:3000Heimdall API base URL
NEXT_PUBLIC_GRAPHQL_URLhttp://localhost:3000/v1/gqlGraphQL endpoint
NEXT_PUBLIC_WS_URLws://localhost:3000/v1/wsWebSocket endpoint
NEXT_PUBLIC_ID_URLhttp://localhost:3002ID app URL (app switcher)
NEXT_PUBLIC_POLICIES_URLhttp://localhost:3004Policies app URL
NEXT_PUBLIC_CONSOLE_URLhttp://localhost:3001Console/backend app URL
NEXT_PUBLIC_DOCS_URLhttp://localhost:3003Docs app URL
NEXT_PUBLIC_MAIN_URLhttp://localhost:3005Main site URL
SYSTEM_API_KEYServer-only system API key (*:*) for NextAuth adapter calls
SOURCE_SERVICEbackend / id / policiesIdentifies the app in audit logs (X-Source-Service)
NEXT_PUBLIC_TURNSTILE_SITE_KEYCloudflare Turnstile site key (empty disables)
NEXT_PUBLIC_GA_MEASUREMENT_IDGoogle Analytics ID (statistics-consent gated)
NEXT_PUBLIC_FB_PIXEL_IDFacebook Pixel ID (optional)
NEXT_PUBLIC_HOTJAR_SITE_IDHotjar site ID (optional)
NEXT_PUBLIC_PLAUSIBLE_DOMAINPlausible domain (optional)
NEXT_PUBLIC_SENTRY_DSNSentry DSN (functional-consent gated)
SENTRY_ORGSentry org (build/source maps)
SENTRY_PROJECTSentry project
SENTRY_AUTH_TOKENSentry auth token
NEXT_PUBLIC_SENTRY_DEBUGfalseEnable Sentry in development

App-specific defaults

AppNEXTAUTH_URL portHEIMDALL_CLIENT_IDSOURCE_SERVICE
backend3001heimdall-backend-clientbackend
id3002heimdall-id-clientid
policies3004heimdall-policies-clientpolicies

Backend-only

VariableExampleDescription
NEXT_PUBLIC_HEIMDALL_CLIENT_IDheimdall-backend-clientPublic client ID for WebSocket consent-revocation detection

Policies-only

VariableExampleDescription
NEXT_PUBLIC_HEIMDALL_CLIENT_IDheimdall-policies-clientPublic client ID for WebSocket consent-revocation detection

ID-only — user-auth OAuth providers

The ID app brokers social login. Each provider needs a client ID/secret (Steam also needs an API key):

ProviderVariables
TwitchTWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET
GoogleGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
DiscordDISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET
GitHubGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
KickKICK_CLIENT_ID, KICK_CLIENT_SECRET
SteamSTEAM_CLIENT_ID, STEAM_CLIENT_SECRET, STEAM_API_KEY
TrovoTROVO_CLIENT_ID, TROVO_CLIENT_SECRET

These ID-app provider OAuth credentials are separate from the API's [integrations.*] platform OAuth, which connects channel/bot features.


Webapp Runtime Configuration (Next.js apps)

The Next.js apps (platform/backend, platform/id, platform/policies) resolve their service URLs at request time, not at build time. This lets a single Docker image be built once and re-pointed per environment via runtime env vars — no rebuild required.

Single source: src/lib/config.ts

Each app has a src/lib/config.ts that is the single source of env resolution. It exposes getter functions; each getter resolves its value at call time with this priority:

  1. HEIMDALL_* — runtime override, set at container start, server-only (wins)
  2. NEXT_PUBLIC_* — build-time fallback, baked into the bundle (client fallback)
  3. localhost default — local dev
HEIMDALL_* → NEXT_PUBLIC_* → localhost default
(runtime) (build-time) (dev)

The server evaluates HEIMDALL_* at request time (runtime config), while the browser has no access to server-only HEIMDALL_* vars and therefore falls back to the build-time NEXT_PUBLIC_* value. This split removes any SSR requirement for client code.

Getters (verified from platform/id/src/lib/config.ts)

GetterResolvesExample value
getApiUrl()API base host (no /v1)http://localhost:3000
getGraphqlUrl()GraphQL endpointhttp://localhost:3000/v1/gql
getWsUrl()WebSocket endpointws://localhost:3000/v1/ws
getIdUrl()ID app URLhttp://localhost:3002
getConsoleUrl()Console/backend app URLhttp://localhost:3001
getDocsUrl()Docs app URLhttp://localhost:3003
getPoliciesUrl()Policies app URLhttp://localhost:3004
getMainUrl()Main site URLhttp://localhost:3005
getSystemApiKey()System API key (server-only, no public fallback → "" on client)
getSourceService()App identifier for audit (SOURCE_SERVICE)id
getCookieDomain()Shared cookie domain (HEIMDALL_COOKIE_DOMAINCOOKIE_DOMAIN)none
getSelfUrl()This app's own URL (NEXTAUTH_URL)http://localhost:3002

getApiUrl() returns the base host only — the app appends paths (/v1/...). Do not point HEIMDALL_API_URL at a /v1 path.

Config-first @elcto/api

The shared @elcto/api library is env-agnostic: it never reads process.env itself. Every transport (apiRequest, graphqlRequest, createWebSocket) and route helper takes an ApiClientConfig as its first argument:

interface ApiClientConfig {
baseUrl: string; // e.g. "http://localhost:3000" (NO trailing /v1)
wsUrl?: string; // e.g. "ws://localhost:3000/v1/ws" (derived from baseUrl if absent)
systemApiKey?: string; // service-to-service / SSR calls
}

Each app builds this from config.ts via getApiConfig() in src/lib/api/index.ts and passes it to every API call:

// platform/id/src/lib/api/index.ts
export function getApiConfig(): ApiClientConfig {
return { baseUrl: getApiUrl(), wsUrl: getWsUrl(), systemApiKey: getSystemApiKey() || undefined };
}

On the client, getSystemApiKey() returns "" (no public fallback) → undefined, so the system key never reaches the browser. See the API Library docs for the transport APIs.

Runtime override vars (HEIMDALL_*)

All optional and server-only. Each app's .env.example documents the same set. When set, each takes precedence over the matching NEXT_PUBLIC_* build-time value.

VariableOverridesMaps to getter
HEIMDALL_API_URLNEXT_PUBLIC_API_URLgetApiUrl()
HEIMDALL_GRAPHQL_URLNEXT_PUBLIC_GRAPHQL_URLgetGraphqlUrl()
HEIMDALL_WS_URLNEXT_PUBLIC_WS_URLgetWsUrl()
HEIMDALL_ID_URLNEXT_PUBLIC_ID_URLgetIdUrl()
HEIMDALL_CONSOLE_URLNEXT_PUBLIC_CONSOLE_URLgetConsoleUrl()
HEIMDALL_DOCS_URLNEXT_PUBLIC_DOCS_URLgetDocsUrl()
HEIMDALL_POLICIES_URLNEXT_PUBLIC_POLICIES_URLgetPoliciesUrl()
HEIMDALL_MAIN_URLNEXT_PUBLIC_MAIN_URLgetMainUrl()
HEIMDALL_COOKIE_DOMAINCOOKIE_DOMAINgetCookieDomain()

Note the distinct prefixes: the API (Rust) uses HEIMDALL__* (double underscore, nested config keys); the webapp runtime overrides use HEIMDALL_* (single underscore, flat URL vars). They are different mechanisms.


Bot Configuration

The bots use the same layered-TOML approach but with their own env prefix and a single underscore (_) separator (not __). Each bot has config/default.toml and an optional config/local.toml.

BotEnv prefixRun-mode envSeparator
DiscordDISCORD_BOT_DISCORD_BOT_RUN_MODE_
TwitchTWITCH_BOT__
YouTubeYOUTUBE_BOT__

The Discord bot (platform/discord_bot/src/config/settings.rs) loads config/default.tomlconfig/{RUN_MODE}.tomlDISCORD_BOT_* env vars. Because the separator is a single _, a nested key like bot.token maps to DISCORD_BOT_BOT_TOKEN:

TOML keyEnv var
bot.tokenDISCORD_BOT_BOT_TOKEN
api.keyDISCORD_BOT_API_KEY
sentry.dsnDISCORD_BOT_SENTRY_DSN

Discord bot config sections (default.toml): environment, [bot] (prefix, default_locale, token), [api] (graphql_endpoint, rest_endpoint, websocket_endpoint, key, bind_address, bind_port), [apps], [sentry] (dsn, traces_sample_rate), [logging] (level).

The Twitch and YouTube bots ship the same TOML layout (referencing TWITCH_BOT_* / YOUTUBE_BOT_* env vars in their config/default.toml) but are currently scaffolds — their main.rs is a stub.


Next Steps