Data & Enterprise
Configuration
Settings in Zornux are typed and declared, not scattered
through the code. A configuration block defines a schema with
defaults; values arrive from data-only files, environment variables, and
overrides — and secrets are redacted everywhere by
default.
A typed schema
Each setting declares a type after as and a default after
is. Instantiate the configuration with create … from
and read settings as fields:
configuration AppConfig
has host as text is "localhost"
has listen_port as whole is 8080
has debug as truth is false
has api_key as secret is "dev-only-secret"
end
create settings from AppConfig
show settings.host # localhost
show settings.listen_port # 8080
Types are text, whole, number,
truth, list, map, and
secret. They're contextual words — text is still
the built-in function everywhere else.
Secrets are redacted
A secret setting never prints in the clear — not with
show, not in JSON. Reading its value is a single, explicit,
auditable act: reveal.
show settings.api_key # ******** (redacted)
show reveal(settings.api_key) # the actual value — the one way in
Setting a secret in a checked-in config file is flagged (ZX2609). Put secrets in environment variables or the local, git-ignored file instead. Using a secret where plain text is required raises ZX2610 until you reveal it.
External secret providers
A secret's value can be a secret://<reference> pointer
instead of a literal. At startup the host resolves it through the configured
secrets_provider — vault,
aws-secrets-manager, azure-key-vault,
gcp-secret-manager, or http — so real secrets live
in your secret store, never in a file. A secret:// reference
with no provider configured stops startup rather than running unresolved.
Files, profiles & overrides
Configuration values are layered, from lowest to highest precedence:
| Layer | Source |
|---|---|
| Schema defaults | The is … values in the configuration block. |
| Profile files | .zxcfg data files, per environment profile. |
| Environment | ZORNUX_* variables. |
| Overrides | --config name=value on the command line. |
The four profiles are Development, Testing,
Staging, and Production, selected with
--profile or ZORNUX_PROFILE. A
.zxcfg file is data only — no code runs in it:
host is "0.0.0.0"
listen_port is 443
debug is false
Injecting configuration
A service reads settings through dependency
injection — use AppConfig makes the loaded configuration
available like any other dependency:
service Greeter
use AppConfig
function greet
give back AppConfig.greeting
end
end
A service can even take its port from configuration:
publish Api on port AppConfig.listen_port.
Where a host listens, and which hosts it serves
Two settings, because they answer two different questions. The listening
port is neither of them — that comes from
publish <Service> on port <N> in the program.
bind_address is "127.0.0.1"
accepted_hosts is "store.example.com, 127.0.0.1"
trusted_proxy_hops is 1
| Setting | Means | Default |
|---|---|---|
bind_address |
Where the process listens, written as an address.
127.0.0.1 is this machine only — what a replica behind a
same-host proxy uses; 0.0.0.0 (or *,
+, all) is every interface. A name is
refused: localhost resolves to [::1] on an
IPv6 host, which is exactly how a deployment ends up bound where its
proxy is not connecting.
|
localhost |
accepted_hosts |
Which Host values the application serves,
matched exactly after lower-casing and dropping the port and any
trailing dot. Empty serves any; * serves any, spelled out.
An unlisted host is answered 421 before it reaches your
code.
|
(empty) |
trusted_proxy_hops |
How many reverse proxies sit in front. 0 ignores
forwarding headers entirely. Behind the generated nginx, set
1. Never set it higher than the proxies you actually
operate — the surplus lets a caller forge both their address and their
host.
|
0 |
It is reported as ZX2713 rather than quietly falling back, because a narrower fallback serves nobody and a wider one opens a port nobody asked for. IPv6 literals are refused with their real reason — this host listens on IPv4, and a proxy in front still serves IPv6 clients.
listen_host still works and still means the address to bind. It used to also become the only Host the process accepted, so a replica bound to 127.0.0.1 answered 404 to everything a proxy forwarded. It no longer does. Setting it alongside bind_address is reported (ZX2715).
Behind a proxy, the Host on the wire is not the caller's: a
same-host proxy sends the replica's bind address as Host and the
caller's real host in X-Forwarded-Host. With
trusted_proxy_hops set, Zornux reads the forwarded one, checks it
against accepted_hosts, and presents it as
request.host. A forwarded host
that is present but unusable is a refusal — never a fall back to the address
on the wire.
Inspecting configuration
Two CLI commands make configuration visible without running the program:
zornux config show prints the resolved settings (secrets
redacted), and zornux config validate checks the files
against every declared schema.
Reserved host settings like log_level, log_output, log_file, job_workers, secrets_provider, and the OpenTelemetry endpoints traces_endpoint / metrics_endpoint are recognized by config validate — a bad value warns (and the run falls back) rather than being reported as an unknown setting. The web host adds bind_address, accepted_hosts, listen_host (superseded), max_request_body_bytes, request_timeout_seconds, header_read_timeout_seconds, idle_connection_timeout_seconds, shutdown_grace_period_seconds, trusted_proxy_hops, static_files, and spa_fallback.
When a host arms the capability-gated key-management (kms) module, these settings configure it: kms_provider, kms_endpoint, kms_family, kms_region, kms_allowed_key_ids, kms_encrypt_purposes / kms_decrypt_purposes, and kms_workload_token. They stay inert until a provider is armed — fail-closed by default.
Diagnostics are ZX2600–ZX2699. Next: shaping requests before they reach a route — Middleware.