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 #

  1. StaticHUSARIA_* (and a handful of supporting vars like ENVIRONMENT, DATABASE_URL) read at startup.
  2. Runtimehusaria_data.runtime_settings overrides a curated subset at runtime via the console Settings tab. Live-applied settings fire observers immediately; restart-only settings persist but require a reboot.
  3. 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 #

VariableDefaultNotes
HUSARIA_HOST0.0.0.0Bind address.
HUSARIA_PORT8080HTTP port.
HUSARIA_READ_TIMEOUT / _WRITE_TIMEOUT / _IDLE_TIMEOUT / _SHUTDOWN_TIMEOUT30s / 30s / 120s / 30sLifecycle timeouts.
HUSARIA_MAX_HEADER_BYTES1048576Max request header size.
HUSARIA_MAX_REQUEST_BYTES1048576Max request body size.
HUSARIA_ENABLE_CONSOLEfalseMount /console UI and /console/api/*.
HUSARIA_ENABLE_INTROSPECTIONtrueAllow __schema / __type on /v1/graphql.
HUSARIA_CORS_ALLOWED_ORIGINSrequired in prodComma-separated.
HUSARIA_CORS_ALLOWED_HEADERSContent-Type,AuthorizationAllowed inbound request headers.
HUSARIA_TRUSTED_PROXIESunsetCIDRs trusted as proxies.
HUSARIA_READ_ONLYfalseRun 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.

VariableDefaultWhat it controls
ENVIRONMENTunset → developmentLogging mode. production selects ProductionConfig (JSON, Level=info, sampling on). Anything else uses DevelopmentConfig (console, Level=debug).
HUSARIA_LOG_LEVELfalls back to whatever ENVIRONMENT pickedLog level override. Replaces the level from ENVIRONMENT. Valid: debug / info / warn / error / fatal.
HUSARIA_GIN_DEBUGfalseGin 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_ENVunsetRead 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 #

VariableDefault
DATABASE_URLunset — full DSN; takes priority.
HUSARIA_DB_HOST / _PORT / _NAME / _USER / _PASSWORD / _SSL_MODElocalhost / 5432 / husaria / husaria / changeme / disable
HUSARIA_DB_MAX_OPEN_CONNS25
HUSARIA_DB_MAX_IDLE_CONNS25
HUSARIA_DB_CONN_MAX_LIFETIME1h
HUSARIA_DB_CONN_MAX_IDLE_TIME10m
HUSARIA_DB_QUERY_TIMEOUT30s

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:

  1. HUSARIA_DB_SSL_MODE if set.
  2. 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.

VariableDefaultNotes
HUSARIA_MEMORY_LIMITunset → 80% of cgroup memory.max when readableSoft memory ceiling (GOMEMLIMIT). Accepts decimal (500M, 2G) and binary (256Mi, 1Gi) suffixes. Stops OOMKills from late GC on small pods.
HUSARIA_GOGC50 with a memory limit, 75 otherwiseHeap growth percent between GCs (GOGC). Lower = heap stays close to the live set, more CPU on GC.
HUSARIA_FREE_OS_MEM_INTERVAL60s with a memory limit, 0 (off) otherwisePeriodic 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:

  1. TCP RemoteAddr is consulted first. If the transport is loopback (127.0.0.0/8, ::1) or matches a CIDR in HUSARIA_TRUSTED_PROXIES, the request bypasses the limiter entirely.
  2. Otherwise, X-Forwarded-For (then X-Real-IP, then RemoteAddr) 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.

VariableDefaultNotes
HUSARIA_ADMIN_SECRETunset → OPEN MODESet to enter SECURED mode.
HUSARIA_ALLOW_OPEN_MODEfalseExplicit 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_ENABLEtrueMount admin API.
HUSARIA_ADMIN_PATH/adminAdmin API path prefix.
HUSARIA_ADMIN_ALLOWED_IPS127.0.0.1,::1IP allow-list.
HUSARIA_JWT_SECRETunsetHS256 signing key.
HUSARIA_JWT_PUBLIC_KEY_PATHunsetRS256 verify key (PEM). Misconfig is fatal.
HUSARIA_JWT_PRIVATE_KEY_PATHunsetRS256 sign key (PEM). Optional.
HUSARIA_JWT_ISSUERunsetRequired iss claim gate.
HUSARIA_JWT_AUDIENCEunsetComma-separated allow-list.
HUSARIA_UNAUTHORIZED_ROLEanonymousRole applied to unauthenticated requests.
HUSARIA_DEFAULT_ROLEuserDefault 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).

VariableDefaultNotes
HUSARIA_AUTH_ALLOW_PRIVATE_HOSTSfalseAllow the auth subsystem (JWKS fetch, auth webhooks) to reach private hosts (internal/auth/httpclient.go:16).
HUSARIA_ACTIONS_ALLOW_PRIVATE_HOSTSfalseAllow action webhooks to reach private hosts (internal/actions/httpclient.go:15).
HUSARIA_DELEGATION_ALLOW_PRIVATE_HOSTSfalseAllow delegation and remote-schema fetches to reach private hosts (internal/delegation/discovery.go:57, shared by internal/remoteschema/client.go:178).

Query allow-list #

VariableDefaultNotes
HUSARIA_ALLOWLIST_MODEdisabledBoot value for allow-list / persisted-query enforcement (cmd/husaria/main.go:404internal/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 #

VariableDefault
HUSARIA_WEBSOCKET_ENABLEtrue
HUSARIA_WEBSOCKET_PORT8081 (used only when distinct from the HTTP port)
HUSARIA_WEBSOCKET_PATH/v1/graphql/ws
HUSARIA_LIVE_QUERIES_REFETCH_INTERVAL_MS1000
HUSARIA_GRAPHQL_CONNECTION_INIT_WAIT_TIMEOUT3s

Cache #

VariableDefault
REDIS_HOST / REDIS_PORTlocalhost / 6379
REDIS_PASSWORDunset
DISABLE_CACHEfalse

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 #

VariableDefault
HUSARIA_AUTO_MIGRATEfalse (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 #

VariableDefault
HUSARIA_METRICS_ENABLEtrue
HUSARIA_METRICS_PATH/metrics

Imported-metadata compatibility (opt-in) #

VariableDefaultNotes
HUSARIA_IMPORT_COMPATfalseRead 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.