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:
config/default.toml— base defaults (required, load fails if missing)config/{APP_ENV}.toml— run-mode overrides, e.g.production.toml(optional)config/local.toml— local developer overrides, git-ignored (optional)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
__:
| TOML | Environment variable |
|---|---|
database.url | HEIMDALL__DATABASE__URL |
server.port | HEIMDALL__SERVER__PORT |
email.smtp.host | HEIMDALL__EMAIL__SMTP__HOST |
storage.upload_limits.images.max_size_mb | HEIMDALL__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_originsemail.supported_localesstorage.upload_limits.images.allowed_typesstorage.upload_limits.documents.allowed_typesstorage.upload_limits.videos.allowed_typesstorage.upload_limits.general.allowed_types
Example: HEIMDALL__CORS__ALLOWED_ORIGINS="https://a.com,https://b.com".
default.tomlThe 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]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
host | string | required (127.0.0.1) | Bind address | HEIMDALL__SERVER__HOST |
port | u16 | required (3000) | Bind port | HEIMDALL__SERVER__PORT |
workers | usize? | none (4) | Actix worker threads | HEIMDALL__SERVER__WORKERS |
keep_alive | u64 | 75 | Keep-alive timeout (seconds) | HEIMDALL__SERVER__KEEP_ALIVE |
public_url | string | http://localhost:3000 | Public base URL for OAuth redirects / external links | HEIMDALL__SERVER__PUBLIC_URL |
[database] (PostgreSQL)
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
url | string | required | PostgreSQL connection URL | HEIMDALL__DATABASE__URL |
max_connections | u32 | required (10) | Max pool connections | HEIMDALL__DATABASE__MAX_CONNECTIONS |
min_connections | u32 | required (2) | Min idle connections | HEIMDALL__DATABASE__MIN_CONNECTIONS |
connect_timeout | u64 | required (30) | Connect timeout (seconds) | HEIMDALL__DATABASE__CONNECT_TIMEOUT |
idle_timeout | u64 | required (600) | Idle timeout (seconds) | HEIMDALL__DATABASE__IDLE_TIMEOUT |
max_lifetime | u64 | 1800 | Max connection lifetime (seconds) | HEIMDALL__DATABASE__MAX_LIFETIME |
[timescale] (TimescaleDB — GPS & audit time-series)
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
url | string | required | TimescaleDB connection URL (default port 5433) | HEIMDALL__TIMESCALE__URL |
max_connections | u32 | 5 | Max pool connections | HEIMDALL__TIMESCALE__MAX_CONNECTIONS |
min_connections | u32 | 1 | Min idle connections | HEIMDALL__TIMESCALE__MIN_CONNECTIONS |
connect_timeout | u64 | 30 | Connect timeout (seconds) | HEIMDALL__TIMESCALE__CONNECT_TIMEOUT |
idle_timeout | u64 | 600 | Idle timeout (seconds) | HEIMDALL__TIMESCALE__IDLE_TIMEOUT |
max_lifetime | u64 | 1800 | Max connection lifetime (seconds) | HEIMDALL__TIMESCALE__MAX_LIFETIME |
compression_after_hours | u32 | 168 | Compress chunks older than N hours (7 days) | HEIMDALL__TIMESCALE__COMPRESSION_AFTER_HOURS |
[redis]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
url | string | required | Redis connection URL | HEIMDALL__REDIS__URL |
pool_size | usize | required (10) | Max pool connections | HEIMDALL__REDIS__POOL_SIZE |
timeout | u64 | 5 | Connection timeout (seconds) | HEIMDALL__REDIS__TIMEOUT |
[graphql]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
playground | bool | true | Serve the GraphiQL playground UI at /v1/graphiql. Implies schema_sdl (GraphiQL fetches /v1/schema). | HEIMDALL__GRAPHQL__PLAYGROUND |
schema_sdl | bool | true | Serve the GraphQL SDL at /v1/schema (REST counterpart of [docs].openapi_json). Also served when playground = true. | HEIMDALL__GRAPHQL__SCHEMA_SDL |
path | string | /graphql (toml: /gql) | GraphQL endpoint path | HEIMDALL__GRAPHQL__PATH |
max_depth | u32 | 10 | Max query depth | HEIMDALL__GRAPHQL__MAX_DEPTH |
max_complexity | u32 | 100 | Max query complexity | HEIMDALL__GRAPHQL__MAX_COMPLEXITY |
[docs] (REST API documentation surface)
Config-gated, environment-independent toggles for the REST docs endpoints. Both
are unauthenticated when enabled. Shipped production.toml and staging.toml
turn swagger_ui and openapi_json off; default.toml (dev) leaves them on.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
swagger_ui | bool | true | Serve the interactive Swagger UI at /v1/swagger-ui/. Implies openapi_json (the UI needs a fetchable spec). | HEIMDALL__DOCS__SWAGGER_UI |
openapi_json | bool | true | Serve the standalone OpenAPI 3.1 JSON spec at /v1/openapi.json. Also served when swagger_ui = true. | HEIMDALL__DOCS__OPENAPI_JSON |
When a surface is disabled its endpoint returns 404 Not Found. The gating is
config-driven (not tied to [app].environment): the handlers read the loaded
config, and requests with no config resolve fail-closed to 404.
[websocket]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
path | string | /ws | WebSocket endpoint path | HEIMDALL__WEBSOCKET__PATH |
heartbeat_interval | u64 | 5 | Heartbeat interval (seconds) | HEIMDALL__WEBSOCKET__HEARTBEAT_INTERVAL |
client_timeout | u64 | 10 | Client inactivity timeout (seconds) | HEIMDALL__WEBSOCKET__CLIENT_TIMEOUT |
max_message_size | usize | 65536 | Max message size (bytes, 64 KB) | HEIMDALL__WEBSOCKET__MAX_MESSAGE_SIZE |
max_frame_size | usize | 65536 | Max frame size (bytes, 64 KB); hard codec limit, must be ≥ max_message_size | HEIMDALL__WEBSOCKET__MAX_FRAME_SIZE |
max_channels_per_session | usize | 100 | Max channels per WS session | HEIMDALL__WEBSOCKET__MAX_CHANNELS_PER_SESSION |
ticket_ttl_secs | u64 | 30 | Lifetime (seconds) of a single-use WS handshake ticket minted via POST /v1/ws/ticket / mintWsTicket (Ticket-Era auth) | HEIMDALL__WEBSOCKET__TICKET_TTL_SECS |
max_connection_lifetime_secs | u64 | 3600 | Max lifetime (seconds) of a single WS connection; the server stops the socket afterwards and the client re-handshakes with a fresh ticket | HEIMDALL__WEBSOCKET__MAX_CONNECTION_LIFETIME_SECS |
max_connections_per_ip | u32 | 20 | Max concurrent WS connections per client IP for authenticated sessions | HEIMDALL__WEBSOCKET__MAX_CONNECTIONS_PER_IP |
max_anon_connections_per_ip | u32 | 5 | Max concurrent WS connections per client IP for anonymous (unauthenticated) sessions — smaller budget than authenticated clients | HEIMDALL__WEBSOCKET__MAX_ANON_CONNECTIONS_PER_IP |
Ticket auth: browsers mint a short-lived single-use ticket over their own bearer
credential (POST /v1/ws/ticket or GraphQL mintWsTicket) and carry it as a
Sec-WebSocket-Protocol entry on the /v1/ws upgrade — the raw credential never
appears in the WS URL or logs. See the WebSocket API.
[auth]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
jwt_secret | string | required (empty) | JWT signing secret — must be overridden | HEIMDALL__AUTH__JWT_SECRET |
jwt_expiration | i64 | 86400 | Access token TTL (seconds, 24h) | HEIMDALL__AUTH__JWT_EXPIRATION |
refresh_token_expiration | i64 | 2592000 | Refresh token TTL (seconds, 30d) | HEIMDALL__AUTH__REFRESH_TOKEN_EXPIRATION |
totp_encryption_key | string | "" | Base64 AES-256 key for TOTP secrets (openssl rand -base64 32) | HEIMDALL__AUTH__TOTP_ENCRYPTION_KEY |
[cors]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
allowed_origins | string[] | required | Allowed origins (comma-separated for env) | HEIMDALL__CORS__ALLOWED_ORIGINS |
allow_credentials | bool | true | Allow credentials in CORS | HEIMDALL__CORS__ALLOW_CREDENTIALS |
max_age | u64 | 3600 | Preflight cache max-age (seconds) | HEIMDALL__CORS__MAX_AGE |
allowed_origins must include every webapp origin that performs a browser request
against the API. In particular the WebSocket ticket mint POST /v1/ws/ticket is a
browser fetch from the webapps, so those origins must be allowed or the mint (and thus
the WS handshake) is CORS-blocked. production.toml ships the prod origins; staging must
supply its origins via HEIMDALL__CORS__ALLOWED_ORIGINS (staging inherits default.toml's
localhost origins otherwise).
[rate_limiting]
Auth-context-aware rate limiting (heimdall-rest::rate_limit, RateLimitMiddleware).
Fixed-window Redis counter, keyed per identity/tier — runs after AuthMiddleware so the
caller's AuthContext (and therefore tier) is known.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
window_secs | u64 | 60 | Window length (seconds); applies to all tiers | HEIMDALL__RATE_LIMITING__WINDOW_SECS |
apikey_max_requests | u64 | 1200 | Max requests/window for non-system API keys | HEIMDALL__RATE_LIMITING__APIKEY_MAX_REQUESTS |
user_max_requests | u64 | 600 | Max requests/window for authenticated users | HEIMDALL__RATE_LIMITING__USER_MAX_REQUESTS |
anonymous_max_requests | u64 | 120 | Max requests/window for anonymous (unauthenticated) requests | HEIMDALL__RATE_LIMITING__ANONYMOUS_MAX_REQUESTS |
internal_token | string? | "" (disabled) | Shared secret; a matching X-Internal-Token header skips rate limiting, anonymous tier only. Empty = bypass disabled (fail-safe). Never commit a real value — set per-environment via env (openssl rand -hex 32) | HEIMDALL__RATE_LIMITING__INTERNAL_TOKEN |
Tiers (heimdall_db::models::RateTier, resolved from AuthContext)
| Tier | Who | Limit |
|---|---|---|
Unlimited | System API key (is_system = true) | ∞ — hardcoded, short-circuited before the Redis call |
ApiKey | Non-system API key | apikey_max_requests |
User | Logged-in user (session/JWT) | user_max_requests |
Anonymous | No auth context | anonymous_max_requests |
Redis key: heimdall:ratelimit:inbound:{apikey:{id}\|user:{uid}\|anon:{ip}}.
Headers: rate-limited responses carry X-RateLimit-Limit, X-RateLimit-Remaining,
X-RateLimit-Reset (excluded: unlimited/system keys, exempt paths, fail-open). Exceeding the limit returns 429 Too Many Requests plus Retry-After
(seconds until the window resets).
Exempt paths (never limited): /health, /v1/health, /v1/ws, /v1/openapi.json,
/v1/schema, and everything under /v1/swagger-ui. OAuth endpoints (/v1/oauth/*) are
limited.
Fail-open: if Redis is unreachable, the middleware logs a warning and allows the request through rather than taking the API down.
[logging]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
level | string | required (debug) | Log level: trace/debug/info/warn/error | HEIMDALL__LOGGING__LEVEL |
format | string | required (pretty) | Log format: json or pretty | HEIMDALL__LOGGING__FORMAT |
[app]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
environment | string | required (development) | App environment: development/staging/production | HEIMDALL__APP__ENVIRONMENT |
debug | bool | true | Debug mode | HEIMDALL__APP__DEBUG |
name | string | required (Heimdall API) | Application name | HEIMDALL__APP__NAME |
[apps]
Frontend URLs used for OAuth client redirect URIs on first launch.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
backend_url | string | http://localhost:3001 | Backend/console app URL | HEIMDALL__APPS__BACKEND_URL |
id_url | string | http://localhost:3002 | ID (login) app URL | HEIMDALL__APPS__ID_URL |
policies_url | string | http://localhost:3004 | Policies app URL | HEIMDALL__APPS__POLICIES_URL |
[bots]
Optional bot health-check endpoints.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
discord_bot_url | string? | none | Discord bot health URL (e.g. http://localhost:3006/health) | HEIMDALL__BOTS__DISCORD_BOT_URL |
twitch_bot_url | string? | none | Twitch bot health URL (e.g. http://localhost:3007/health) | HEIMDALL__BOTS__TWITCH_BOT_URL |
[scheduler]
The whole section is optional (Option<SchedulerConfig>).
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | true | Enable background scheduler | HEIMDALL__SCHEDULER__ENABLED |
deletion_cron | string? | 0 */15 * * * * | Account-deletion job cron (every 15 min) | HEIMDALL__SCHEDULER__DELETION_CRON |
integration_refresh_cron | string? | 0 */5 * * * * | Integration token-refresh cron (every 5 min) | HEIMDALL__SCHEDULER__INTEGRATION_REFRESH_CRON |
integration_refresh_buffer_minutes | i64? | 15 | Refresh this many minutes before token expiry | HEIMDALL__SCHEDULER__INTEGRATION_REFRESH_BUFFER_MINUTES |
channel_stats_refresh_cron | string? | 0 0 */6 * * * | Channel-stats refresh cron (every 6h) | HEIMDALL__SCHEDULER__CHANNEL_STATS_REFRESH_CRON |
channel_stats_stale_minutes | i64? | 360 | Minutes 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.tomlshipsdeletion_cron = "0 */1 * * * *".
[email]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable email sending | HEIMDALL__EMAIL__ENABLED |
provider | string | console | sendgrid, smtp, or console | HEIMDALL__EMAIL__PROVIDER |
from_email | string | noreply@elcapitano.com | Sender address | HEIMDALL__EMAIL__FROM_EMAIL |
from_name | string | elcapitano Identity | Sender display name | HEIMDALL__EMAIL__FROM_NAME |
token_expiry_hours | u32 | 24 | Verification-link TTL (hours) | HEIMDALL__EMAIL__TOKEN_EXPIRY_HOURS |
default_locale | string | en | Fallback email locale | HEIMDALL__EMAIL__DEFAULT_LOCALE |
supported_locales | string[] | ["en","de"] | Supported email locales (comma-separated for env) | HEIMDALL__EMAIL__SUPPORTED_LOCALES |
[email.sendgrid]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
api_key | string | "" | SendGrid API key | HEIMDALL__EMAIL__SENDGRID__API_KEY |
[email.smtp]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
host | string | localhost | SMTP host | HEIMDALL__EMAIL__SMTP__HOST |
port | u16 | 587 | SMTP port (587 STARTTLS / 465 SSL / 25 plain) | HEIMDALL__EMAIL__SMTP__PORT |
username | string | "" | SMTP username | HEIMDALL__EMAIL__SMTP__USERNAME |
password | string | "" | SMTP password | HEIMDALL__EMAIL__SMTP__PASSWORD |
tls | bool | true | Use TLS/STARTTLS | HEIMDALL__EMAIL__SMTP__TLS |
timeout | u64 | 30 | Connection timeout (seconds) | HEIMDALL__EMAIL__SMTP__TIMEOUT |
[turnstile]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable Cloudflare Turnstile verification | HEIMDALL__TURNSTILE__ENABLED |
secret_key | string | "" | Turnstile server-side secret key | HEIMDALL__TURNSTILE__SECRET_KEY |
[sentry]
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable Sentry error tracking | HEIMDALL__SENTRY__ENABLED |
dsn | string | "" | Sentry DSN | HEIMDALL__SENTRY__DSN |
environment | string | development | Sentry environment name | HEIMDALL__SENTRY__ENVIRONMENT |
traces_sample_rate | f32 | 1.0 | Transaction sample rate (0.0–1.0) | HEIMDALL__SENTRY__TRACES_SAMPLE_RATE |
sample_rate | f32 | 1.0 | Error-event sample rate (0.0–1.0) | HEIMDALL__SENTRY__SAMPLE_RATE |
debug | bool | false | Sentry SDK debug mode | HEIMDALL__SENTRY__DEBUG |
attach_stacktrace | bool | true | Attach stack traces to messages | HEIMDALL__SENTRY__ATTACH_STACKTRACE |
send_default_pii | bool | false | Send PII in error reports | HEIMDALL__SENTRY__SEND_DEFAULT_PII |
max_breadcrumbs | usize | 100 | Max breadcrumbs captured | HEIMDALL__SENTRY__MAX_BREADCRUMBS |
[geoip]
MaxMind GeoLite2 IP-to-location lookups used for audit enrichment.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Master switch; when off no DB is loaded and location fields stay empty | HEIMDALL__GEOIP__ENABLED |
database_path | string? | none | Path to GeoLite2 City .mmdb | HEIMDALL__GEOIP__DATABASE_PATH |
account_id | string? | none | MaxMind account ID (Privacy Exclusions API) | HEIMDALL__GEOIP__ACCOUNT_ID |
license_key | string? | none | MaxMind license key (auto-update / Privacy Exclusions) | HEIMDALL__GEOIP__LICENSE_KEY |
auto_update | bool | false | Auto-update DB on startup (needs license key) | HEIMDALL__GEOIP__AUTO_UPDATE |
update_interval_days | u32 | 7 | DB update interval (days) | HEIMDALL__GEOIP__UPDATE_INTERVAL_DAYS |
privacy_exclusions_enabled | bool | false | Enable Privacy Exclusions API | HEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_ENABLED |
privacy_exclusions_refresh_hours | u32 | 24 | Refresh interval for exclusions (hours) | HEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_REFRESH_HOURS |
privacy_exclusions_cache_path | string | data/geoip-privacy-exclusions.json | Local cache file path | HEIMDALL__GEOIP__PRIVACY_EXCLUSIONS_CACHE_PATH |
[integrations]
Streaming-platform OAuth (Twitch, YouTube, Kick, Trovo) — separate from user auth.
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
token_encryption_key | string | "" | AES-256-GCM key for stored OAuth tokens (openssl rand -base64 32) | HEIMDALL__INTEGRATIONS__TOKEN_ENCRYPTION_KEY |
internal_service_key | string | "" | 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:
| Key | Type | Default | Description | Env override (example: twitch) |
|---|---|---|---|---|
client_id | string | "" | OAuth client ID | HEIMDALL__INTEGRATIONS__TWITCH__CLIENT_ID |
client_secret | string | "" | OAuth client secret | HEIMDALL__INTEGRATIONS__TWITCH__CLIENT_SECRET |
Swap
TWITCHforYOUTUBE,KICK, orTROVOfor the other platforms.
[storage] (S3-compatible / local filesystem)
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable storage service | HEIMDALL__STORAGE__ENABLED |
backend | string | "s3" (toml: "local") | Storage backend: "s3" (S3/MinIO/R2/Spaces) or "local" (filesystem) | HEIMDALL__STORAGE__BACKEND |
local_path | string? | none (toml: ./uploads) | Local filesystem base path; only used when backend = "local" | HEIMDALL__STORAGE__LOCAL_PATH |
endpoint | string | http://localhost:9000 | S3 endpoint URL | HEIMDALL__STORAGE__ENDPOINT |
region | string | us-east-1 (toml: auto) | S3 region | HEIMDALL__STORAGE__REGION |
access_key | string | "" | S3 access key ID | HEIMDALL__STORAGE__ACCESS_KEY |
secret_key | string | "" | S3 secret access key | HEIMDALL__STORAGE__SECRET_KEY |
bucket | string | heimdall | Default bucket | HEIMDALL__STORAGE__BUCKET |
path_style | bool | false | Use path-style URLs (required for MinIO) | HEIMDALL__STORAGE__PATH_STYLE |
public_url | string? | none | Public CDN URL if different from endpoint | HEIMDALL__STORAGE__PUBLIC_URL |
presigned_expiry_seconds | u32 | 3600 | Presigned URL TTL (seconds) | HEIMDALL__STORAGE__PRESIGNED_EXPIRY_SECONDS |
On the local backend, presigned-download returns the direct GET /v1/storage/download/{key} URL (not a signed URL); presigned-upload and multipart uploads are not supported (StorageError::Unsupported). See Storage API for the full operation matrix.
[storage.upload_limits.*]
Four categories — images, documents, videos, general — each with the same keys:
| Key | Type | Default (images / documents / videos / general) | Description |
|---|---|---|---|
max_size_mb | u64 | 5 / 25 / 500 / 100 | Max upload size (MB) |
allowed_types | string[] | 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)
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable Pegelonline polling | HEIMDALL__PEGELONLINE__ENABLED |
base_url | string | https://pegelonline.wsv.de/webservices/rest-api/v2 | Pegelonline REST API base | HEIMDALL__PEGELONLINE__BASE_URL |
radius_km | f64 | 30.0 | Search radius around GPS position (km) | HEIMDALL__PEGELONLINE__RADIUS_KM |
stations | string[] | [] | Station whitelist (exact API shortnames); empty = radius-based | HEIMDALL__PEGELONLINE__STATIONS |
[ais] (vessel tracking)
| Key | Type | Default | Description | Env override |
|---|---|---|---|---|
enabled | bool | false | Enable AIS tracking | HEIMDALL__AIS__ENABLED |
api_key | string | "" | AIS data-provider API key | HEIMDALL__AIS__API_KEY |
radius_km | f64 | 10.0 | Search radius around GPS position (km) | HEIMDALL__AIS__RADIUS_KM |
ignore_mmsi | u32[] | [] | 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
| Variable | Example | Description |
|---|---|---|
NEXTAUTH_URL | http://localhost:3001 | NextAuth base URL (per app port) |
NEXTAUTH_SECRET | — | NextAuth session secret |
HEIMDALL_CLIENT_ID | heimdall-backend-client | OAuth client ID for this app |
HEIMDALL_CLIENT_SECRET | — | OAuth client secret |
NEXT_PUBLIC_API_URL | http://localhost:3000 | Heimdall API base URL |
NEXT_PUBLIC_GRAPHQL_URL | http://localhost:3000/v1/gql | GraphQL endpoint |
NEXT_PUBLIC_WS_URL | ws://localhost:3000/v1/ws | WebSocket endpoint |
NEXT_PUBLIC_ID_URL | http://localhost:3002 | ID app URL (app switcher) |
NEXT_PUBLIC_POLICIES_URL | http://localhost:3004 | Policies app URL |
NEXT_PUBLIC_CONSOLE_URL | http://localhost:3001 | Console/backend app URL |
NEXT_PUBLIC_DOCS_URL | http://localhost:3003 | Docs app URL |
NEXT_PUBLIC_MAIN_URL | http://localhost:3005 | Main site URL |
SYSTEM_API_KEY | — | Server-only system API key (*:*) for NextAuth adapter calls |
SOURCE_SERVICE | backend / id / policies | Identifies the app in audit logs (X-Source-Service) |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | — | Cloudflare Turnstile site key (empty disables) |
NEXT_PUBLIC_GA_MEASUREMENT_ID | — | Google Analytics ID (statistics-consent gated) |
NEXT_PUBLIC_FB_PIXEL_ID | — | Facebook Pixel ID (optional) |
NEXT_PUBLIC_HOTJAR_SITE_ID | — | Hotjar site ID (optional) |
NEXT_PUBLIC_PLAUSIBLE_DOMAIN | — | Plausible domain (optional) |
NEXT_PUBLIC_SENTRY_DSN | — | Sentry DSN (functional-consent gated) |
SENTRY_ORG | — | Sentry org (build/source maps) |
SENTRY_PROJECT | — | Sentry project |
SENTRY_AUTH_TOKEN | — | Sentry auth token |
NEXT_PUBLIC_SENTRY_DEBUG | false | Enable Sentry in development |
App-specific defaults
| App | NEXTAUTH_URL port | HEIMDALL_CLIENT_ID | SOURCE_SERVICE |
|---|---|---|---|
backend | 3001 | heimdall-backend-client | backend |
id | 3002 | heimdall-id-client | id |
policies | 3004 | heimdall-policies-client | policies |
Backend-only
| Variable | Example | Description |
|---|---|---|
NEXT_PUBLIC_HEIMDALL_CLIENT_ID | heimdall-backend-client | Public client ID for WebSocket consent-revocation detection |
Policies-only
| Variable | Example | Description |
|---|---|---|
NEXT_PUBLIC_HEIMDALL_CLIENT_ID | heimdall-policies-client | Public 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):
| Provider | Variables |
|---|---|
| Twitch | TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | |
| Discord | DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET |
| GitHub | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
| Kick | KICK_CLIENT_ID, KICK_CLIENT_SECRET |
| Steam | STEAM_CLIENT_ID, STEAM_CLIENT_SECRET, STEAM_API_KEY |
| Trovo | TROVO_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:
HEIMDALL_*— runtime override, set at container start, server-only (wins)NEXT_PUBLIC_*— build-time fallback, baked into the bundle (client fallback)- 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)
| Getter | Resolves | Example value |
|---|---|---|
getApiUrl() | API base host (no /v1) | http://localhost:3000 |
getGraphqlUrl() | GraphQL endpoint | http://localhost:3000/v1/gql |
getWsUrl() | WebSocket endpoint | ws://localhost:3000/v1/ws |
getIdUrl() | ID app URL | http://localhost:3002 |
getConsoleUrl() | Console/backend app URL | http://localhost:3001 |
getDocsUrl() | Docs app URL | http://localhost:3003 |
getPoliciesUrl() | Policies app URL | http://localhost:3004 |
getMainUrl() | Main site URL | http://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_DOMAIN → COOKIE_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 pointHEIMDALL_API_URLat a/v1path.
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
internalToken?: string; // sent as X-Internal-Token; bypasses rate limiting (anonymous tier only)
sourceService?: string; // sent as X-Source-Service on every request; audit source attribution
}
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,
internalToken: getInternalToken() || undefined,
sourceService: getSourceService(),
};
}
On the client, getSystemApiKey() returns "" (no public fallback) → undefined, so the
system key never reaches the browser. sourceService has no such restriction — it is not a
secret — so it is sent from both server and client contexts.
The GraphQL and REST transports (shared/api/src/clients/graphql.ts, rest.ts) send
options.sourceService ?? config.sourceService as the X-Source-Service header on every
request, so as long as an app builds its config via getApiConfig() the header is present
by default (a per-call sourceService option can still override it). This is what lets the
Rust API attribute the login audit event (emitted at OAuth token exchange) to the calling
webapp instead of the generic "api" source fallback — see
Source & device attribution from webapps.
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.
| Variable | Overrides | Maps to getter |
|---|---|---|
HEIMDALL_API_URL | NEXT_PUBLIC_API_URL | getApiUrl() |
HEIMDALL_GRAPHQL_URL | NEXT_PUBLIC_GRAPHQL_URL | getGraphqlUrl() |
HEIMDALL_WS_URL | NEXT_PUBLIC_WS_URL | getWsUrl() |
HEIMDALL_ID_URL | NEXT_PUBLIC_ID_URL | getIdUrl() |
HEIMDALL_CONSOLE_URL | NEXT_PUBLIC_CONSOLE_URL | getConsoleUrl() |
HEIMDALL_DOCS_URL | NEXT_PUBLIC_DOCS_URL | getDocsUrl() |
HEIMDALL_POLICIES_URL | NEXT_PUBLIC_POLICIES_URL | getPoliciesUrl() |
HEIMDALL_MAIN_URL | NEXT_PUBLIC_MAIN_URL | getMainUrl() |
HEIMDALL_COOKIE_DOMAIN | COOKIE_DOMAIN | getCookieDomain() |
HEIMDALL_INTERNAL_TOKEN | (no public fallback, server-only) | getInternalToken() — must match the API's HEIMDALL__RATE_LIMITING__INTERNAL_TOKEN |
Note the distinct prefixes: the API (Rust) uses
HEIMDALL__*(double underscore, nested config keys); the webapp runtime overrides useHEIMDALL_*(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.
| Bot | Env prefix | Run-mode env | Separator |
|---|---|---|---|
| Discord | DISCORD_BOT_ | DISCORD_BOT_RUN_MODE | _ |
| Twitch | TWITCH_BOT_ | — | _ |
| YouTube | YOUTUBE_BOT_ | — | _ |
The Discord bot (platform/discord_bot/src/config/settings.rs) loads
config/default.toml → config/{RUN_MODE}.toml → DISCORD_BOT_* env vars. Because the
separator is a single _, a nested key like bot.token maps to DISCORD_BOT_BOT_TOKEN:
| TOML key | Env var |
|---|---|
bot.token | DISCORD_BOT_BOT_TOKEN |
api.key | DISCORD_BOT_API_KEY |
sentry.dsn | DISCORD_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 theirconfig/default.toml) but are currently scaffolds — theirmain.rsis a stub.
Next Steps
- Crate Reference —
heimdall-configand the other crates - Databases & Caching — PostgreSQL, TimescaleDB, Redis details
- Email — email provider configuration in depth
- Deployment — build, packaging, and release