Getting started
Configuration
Configuration #
Husaria is configured entirely through environment variables. Every value in this section appears in .env.example or is read directly by cmd/husaria/main.go / internal/server/config.go.
How configuration is layered #
- Static —
HUSARIA_*(and a handful of supporting vars likeENVIRONMENT,DATABASE_URL) read at startup. - Runtime —
husaria_data.runtime_settingsoverrides a curated subset at runtime via the console Settings tab. Live-applied settings fire observers immediately; restart-only settings persist but require a reboot. - Per-request — middleware headers (
X-Husaria-Role,X-Husaria-Admin-Secret,traceparent) influence individual requests.
This page covers the static layer. See Runtime settings for the live-toggleable subset.
Server #
| Variable | Default | Notes |
|---|---|---|
HUSARIA_HOST | 0.0.0.0 | Bind address. |
HUSARIA_PORT | 8080 | HTTP port. |
HUSARIA_READ_TIMEOUT / _WRITE_TIMEOUT / _IDLE_TIMEOUT / _SHUTDOWN_TIMEOUT | 30s / 30s / 120s / 30s | Lifecycle timeouts. |
HUSARIA_MAX_HEADER_BYTES | 1048576 | Max request header size. |
HUSARIA_MAX_REQUEST_BYTES | 1048576 | Max request body size. |
HUSARIA_ENABLE_CONSOLE | false | Mount /console UI and /console/api/*. |
HUSARIA_ENABLE_INTROSPECTION | true | Allow __schema / __type on /v1/graphql. |
HUSARIA_CORS_ALLOWED_ORIGINS | required in prod | Comma-separated. |
HUSARIA_CORS_ALLOWED_HEADERS | Content-Type,Authorization | Allowed inbound request headers. |
HUSARIA_TRUSTED_PROXIES | unset | CIDRs trusted as proxies. |
HUSARIA_READ_ONLY | false | Run as a read-only replica — see Read-only replicas. |
Modes — three env vars, three different concepts #
Three vars look similar but each controls a separate thing. They are independent and combinable.
| Variable | Default | What it controls |
|---|---|---|
ENVIRONMENT | unset → development | Logging mode. production selects ProductionConfig (JSON, Level=info, sampling on). Anything else uses DevelopmentConfig (console, Level=debug). |
HUSARIA_LOG_LEVEL | falls back to whatever ENVIRONMENT picked | Log level override. Replaces the level from ENVIRONMENT. Valid: debug / info / warn / error / fatal. |
HUSARIA_GIN_DEBUG | false | Gin mode. =true puts gin in DebugMode (verbose per-route logging at startup). Also enabled when HUSARIA_ENV=development, unless HUSARIA_GIN_DEBUG=false is set explicitly (the =false opt-out wins). |
HUSARIA_ENV | unset | Read only by the gin-mode logic above. Has no effect on logging. |
For production, set ENVIRONMENT=production (or HUSARIA_LOG_LEVEL=info) and leave the gin vars unset.
Database #
| Variable | Default |
|---|---|
DATABASE_URL | unset — full DSN; takes priority. |
HUSARIA_DB_HOST / _PORT / _NAME / _USER / _PASSWORD / _SSL_MODE | localhost / 5432 / husaria / husaria / changeme / disable |
HUSARIA_DB_MAX_OPEN_CONNS | 25 |
HUSARIA_DB_MAX_IDLE_CONNS | 25 |
HUSARIA_DB_CONN_MAX_LIFETIME | 1h |
HUSARIA_DB_CONN_MAX_IDLE_TIME | 10m |
HUSARIA_DB_QUERY_TIMEOUT | 30s |
POSTGRES_* fallbacks (HOST, PORT, USER, PASSWORD, DB, SSL_MODE) are accepted when the matching HUSARIA_DB_* is unset.
DATABASE_URL sslmode auto-patch #
When you supply DATABASE_URL directly (Compose, Kubernetes secret), Husaria parses it and injects sslmode= if the DSN doesn't carry one. The injected value is:
HUSARIA_DB_SSL_MODEif set.- Otherwise
disable.
This avoids a surprise: lib/pq defaults to sslmode=require, so a clean postgres://user:pass@host:5432/db would try TLS — and the typical in-cluster Postgres (CloudNativePG default, postgres-operator default) ships with SSL disabled, yielding pq: SSL is not enabled on the server. For production against a TLS-terminated Postgres, set HUSARIA_DB_SSL_MODE=require explicitly.
Runtime memory tuning #
Husaria configures Go's GC + heap behaviour at boot, before the first allocations. Defaults are pod-aware — when a memory limit is detected (env or cgroup) the tuner runs in "small pod" mode (lower GOGC, periodic FreeOSMemory); without a limit it runs in "throughput" mode.
| Variable | Default | Notes |
|---|---|---|
HUSARIA_MEMORY_LIMIT | unset → 80% of cgroup memory.max when readable | Soft memory ceiling (GOMEMLIMIT). Accepts decimal (500M, 2G) and binary (256Mi, 1Gi) suffixes. Stops OOMKills from late GC on small pods. |
HUSARIA_GOGC | 50 with a memory limit, 75 otherwise | Heap growth percent between GCs (GOGC). Lower = heap stays close to the live set, more CPU on GC. |
HUSARIA_FREE_OS_MEM_INTERVAL | 60s with a memory limit, 0 (off) otherwise | Periodic debug.FreeOSMemory() cadence. Returns idle spans to the OS so RSS doesn't drift above the live set on a quiet pod. Cheap by design — only releases scavengeable spans. |
Why tune: Go's default GC tuning aims at throughput, not RSS. On a 256–512 Mi pod the runtime will happily overshoot the cgroup ceiling waiting for the next GC and trip OOMKiller. The same defaults on a 4 Gi pod leave CPU on the table running GC too often. The tuner trades a small amount of GC overhead for a tighter RSS ceiling.
The cgroup probe reads both v1 (/sys/fs/cgroup/memory/memory.limit_in_bytes) and v2 (/sys/fs/cgroup/memory.max). When neither is readable (bare-metal, macOS dev), GOMEMLIMIT stays at Go's default (math.MaxInt64) and GOGC defaults to 75.
Middleware behaviour #
Allowed HTTP methods #
The validation middleware accepts GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. HEAD is included so sidecar proxies that probe with HEAD /healthz rather than GET don't bounce off a 405 — see Metrics & health.
Rate-limit identity (transport trust) #
HUSARIA_GRAPHQL_RATE_LIMIT (default 600 req/min) is enforced per client identity. Identity resolution:
- TCP
RemoteAddris consulted first. If the transport is loopback (127.0.0.0/8,::1) or matches a CIDR inHUSARIA_TRUSTED_PROXIES, the request bypasses the limiter entirely. - Otherwise,
X-Forwarded-For(thenX-Real-IP, thenRemoteAddr) feeds the identity.
This ordering matters in sidecar setups: a proxy at 127.0.0.1 fans out client traffic with their X-Forwarded-For values. If the limiter trusted XFF first, the proxy's own loopback IP would never get a chance to be flagged as trusted — instead the proxy's collapsed traffic would look like one identity and trip the per-IP cap. A spoofed XFF from a non-loopback transport still counts as that transport's identity — TCP source always wins for trust decisions.
Authentication #
HUSARIA_ADMIN_SECRET selects the auth mode — see Authentication for the OPEN vs SECURED decision tree.
| Variable | Default | Notes |
|---|---|---|
HUSARIA_ADMIN_SECRET | unset → OPEN MODE | Set to enter SECURED mode. |
HUSARIA_ALLOW_OPEN_MODE | false | Explicit opt-in to OPEN MODE. With HUSARIA_ADMIN_SECRET unset, the server refuses to start unless this is true (cmd/husaria/main.go:121-168). Local-dev only — every caller would act as admin with full RLS bypass. |
HUSARIA_ADMIN_ENABLE | true | Mount admin API. |
HUSARIA_ADMIN_PATH | /admin | Admin API path prefix. |
HUSARIA_ADMIN_ALLOWED_IPS | 127.0.0.1,::1 | IP allow-list. |
HUSARIA_JWT_SECRET | unset | HS256 signing key. |
HUSARIA_JWT_PUBLIC_KEY_PATH | unset | RS256 verify key (PEM). Misconfig is fatal. |
HUSARIA_JWT_PRIVATE_KEY_PATH | unset | RS256 sign key (PEM). Optional. |
HUSARIA_JWT_ISSUER | unset | Required iss claim gate. |
HUSARIA_JWT_AUDIENCE | unset | Comma-separated allow-list. |
HUSARIA_UNAUTHORIZED_ROLE | anonymous | Role applied to unauthenticated requests. |
HUSARIA_DEFAULT_ROLE | user | Default role when JWT lacks default_role. |
SSRF private-host opt-outs #
Husaria's outbound HTTP subsystems (JWKS/auth webhooks, action webhooks, delegation/remote-schema fetches) refuse to reach private/link-local addresses by default to block SSRF. Each subsystem has its own opt-out env var — flip one only when the target genuinely lives on a private network you control (e.g. an in-cluster IDP or webhook).
| Variable | Default | Notes |
|---|---|---|
HUSARIA_AUTH_ALLOW_PRIVATE_HOSTS | false | Allow the auth subsystem (JWKS fetch, auth webhooks) to reach private hosts (internal/auth/httpclient.go:16). |
HUSARIA_ACTIONS_ALLOW_PRIVATE_HOSTS | false | Allow action webhooks to reach private hosts (internal/actions/httpclient.go:15). |
HUSARIA_DELEGATION_ALLOW_PRIVATE_HOSTS | false | Allow delegation and remote-schema fetches to reach private hosts (internal/delegation/discovery.go:57, shared by internal/remoteschema/client.go:178). |
Query allow-list #
| Variable | Default | Notes |
|---|---|---|
HUSARIA_ALLOWLIST_MODE | disabled | Boot value for allow-list / persisted-query enforcement (cmd/husaria/main.go:404 → internal/allowlist/mode.go). Valid: disabled (no gating; APQ still resolves/auto-stores), log-only (gate evaluated, violations logged but allowed), enforce (unknown queries rejected). Also settable live via the allowlist_mode runtime setting, which overrides the boot value. See Allow-list. |
Subscriptions / WebSocket #
| Variable | Default |
|---|---|
HUSARIA_WEBSOCKET_ENABLE | true |
HUSARIA_WEBSOCKET_PORT | 8081 (used only when distinct from the HTTP port) |
HUSARIA_WEBSOCKET_PATH | /v1/graphql/ws |
HUSARIA_LIVE_QUERIES_REFETCH_INTERVAL_MS | 1000 |
HUSARIA_GRAPHQL_CONNECTION_INIT_WAIT_TIMEOUT | 3s |
Cache #
| Variable | Default |
|---|---|
REDIS_HOST / REDIS_PORT | localhost / 6379 |
REDIS_PASSWORD | unset |
DISABLE_CACHE | false |
Husaria probes Redis at startup. When reachable, the response-cache middleware mounts as a hybrid cache (memory + Redis). When unreachable, it falls back transparently to an in-process LRU. Redis keys carry the hsr- prefix.
Migrations #
| Variable | Default |
|---|---|
HUSARIA_AUTO_MIGRATE | false (no-op on read-only replicas) |
HUSARIA_MIGRATIONS_PATH | ./migrations |
Default posture is operator-driven — apply via the console UI or husaria-cli migrate up. See Migrations.
Metrics #
| Variable | Default |
|---|---|
HUSARIA_METRICS_ENABLE | true |
HUSARIA_METRICS_PATH | /metrics |
Imported-metadata compatibility (opt-in) #
| Variable | Default | Notes |
|---|---|---|
HUSARIA_IMPORT_COMPAT | false | Read relationship-name overrides from an imported hdb_catalog.hdb_metadata document. Enable on instances migrated from a prior deployment that stored that document, so client mutations referencing the original rel names (e.g. tg_user rather than the convention-derived user) validate. Off by default — greenfield deployments skip the per-rebuild Postgres round-trip. |
See .env.example in the repository for the full inventory.