Configuration reference
Every SithBit binary follows the same contract: an empty or missing config file is a runnable dev instance. Every setting has an in-code default — loopback binds, unprivileged ports, a local SQLite store — so configuration is only ever overriding a default, never satisfying a required field. The one exception is called out below (TLS certificate paths, which have no sensible default).
The annotated example files are the canonical per-key documentation and ship with every default shown commented out:
mail_spooler/sithbitd.example.tomlaccount_api/account_api.tomldomain_sithbit/domain_sithbit.example.tomlipfs_daemon/sithbit_ipfsd.example.tomlipfs_gateway/ipfs_gateway.example.tomlmail_grpc/mail_grpc.example.tomlpop_server/pop_server.tomlimap_server/imap_server.tomlsmtp_server/smtp_server.tomlmail_migrate/sithbit_migrate.example.tomlmail_console/sithbit_console.toml
The last three ship under the config file’s own name rather than an
.example copy, and are the one place a setting is left live instead
of commented out — see Standalone protocol
servers for the two dev-only exceptions
they make.
How a setting resolves
Eleven binaries take an annotated TOML file of their own — sithbitd,
account-api, domain-sithbit, sithbit-ipfsd, sithbit-gateway,
mail-grpc, the three standalone protocol
servers pop-server, imap-server and
smtp-server, the store-migration tool
sithbit-migrate, and the admin TUI
sithbit-console. Every one of them layers each
setting the same way, lowest to highest precedence:
- the in-code default,
- the TOML file (its own file name in the working directory, or the
path in its
*_CONFIGenvironment variable — both named in the table below), - an optional cloud app-config source — AWS AppConfig or Azure App Configuration, opted into per binary by a bootstrap env var; skipped entirely when neither var is set,
./.env,./.env.$APP_ENV(APP_ENVcomes from the environment or./.env),- the real environment.
Environment variables address individual settings as
{PREFIX}_{PATH}, with __ descending one TOML nesting level:
SITHBITD_STORE__KIND=aws # [store] kind
SITHBITD_SMTP__SERVER__BIND_ADDR=0.0.0.0:2525 # [smtp.server] bind_addr
SITHBITD_STORE__BLOBS__KIND=azure # [store.blobs] kind (tagged enum)
Each binary’s file, config-path variable, and prefix:
| Binary | Config file | Config-path variable | Env prefix |
|---|---|---|---|
sithbitd | sithbitd.toml | SITHBITD_CONFIG | SITHBITD |
account-api | account_api.toml | ACCOUNT_API_CONFIG | ACCOUNT_API |
domain-sithbit | domain_sithbit.toml | DOMAIN_SITHBIT_CONFIG | DOMAIN_SITHBIT |
sithbit-ipfsd | sithbit_ipfsd.toml | SITHBIT_IPFSD_CONFIG | SITHBIT_IPFSD |
sithbit-gateway | ipfs_gateway.toml | IPFS_GATEWAY_CONFIG | IPFS_GATEWAY |
mail-grpc | mail_grpc.toml | MAIL_GRPC_CONFIG | MAIL_GRPC |
pop-server | pop_server.toml | POP_SERVER_CONFIG | POP_SERVER |
imap-server | imap_server.toml | IMAP_SERVER_CONFIG | IMAP_SERVER |
smtp-server | smtp_server.toml | SMTP_SERVER_CONFIG | SMTP_SERVER |
sithbit-migrate | sithbit_migrate.toml | SITHBIT_MIGRATE_CONFIG | SITHBIT_MIGRATE |
sithbit-console | sithbit_console.toml | SITHBIT_CONSOLE_CONFIG | SITHBIT_CONSOLE |
Env files are read from the working directory only, and their values are exported to the process environment without overriding variables that are already set.
Key sources: files or cloud secret managers
Six file-loaded secrets take a key source rather than a bare path:
the account-api JWT signing key (jwt.key_file), the
DKIM signing key(s) ([spooler.dkim] /
[mail.dkim] key_file), the credential-sealing
key (credential_key_file), every
server’s TLS certificate and key
(certs / key, the account-api’s [tls] included — it shares the same section shape),
domain-sithbit’s delegate signing key
(delegate_key_file), and mail-grpc’s signing/fee-payer
keypair (keypair).
A key source is either a local file (the default) or a cloud secret
manager’s secret — Azure Key Vault (akv), AWS Secrets Manager (asm),
or Google Secret Manager (gsm) — in one of these TOML forms:
key_file = "jwt.key" # 1. bare path string -> a local file
key_file = { path = "jwt.key" } # 2. table, kind omitted -> a local file
key_file = { kind = "akv", # 3. an Azure Key Vault secret
vault_uri = "https://<vault>.vault.azure.net/",
secret_name = "jwt-signing-key" }
key_file = { kind = "asm", # 4. an AWS Secrets Manager secret
secret_id = "sithbit/jwt-signing-key" }
key_file = { kind = "gsm", # 5. a Google Secret Manager secret
project = "my-project",
secret = "jwt-signing-key" }
The representation defaults to a file, so a config that names a plain
path — or omits kind — keeps the historical, zero-config file behaviour
byte-for-byte; only an explicit cloud kind opts into a secret manager.
This holds for every key-source field whatever it is named (key_file,
credential_key_file, delegate_key_file, keypair, certs / key).
A cloud secret’s value holds exactly what the file would have — the raw
key bytes, or the PEM. The TLS pair therefore fetches two secrets —
the certificate-chain PEM and the private-key PEM — one per certs/key
entry. No secret material lives in the config
file itself; it carries only the secret’s coordinates. Per cloud:
akvnames thevault_uriandsecret_name. Authentication reuses the same managed-identity credential chain as the Azure storage backend (azure_identity’s ManagedIdentity): no new auth to configure.asmnames thesecret_id— a secret name or a full ARN — with optionalregion(defaults to the ambient AWS configuration’s) andendpoint_url(an emulator such as LocalStack, mirroring the AWS storage backend’s setting). Authentication is the ambient AWS credential chain (environment, profile, IAM role). A string secret’s UTF-8 bytes are used; a binary secret’s raw bytes serve as the fallback when no string value is present.gsmnames theprojectandsecret, with optionalversion(default"latest"). Authentication is Application Default Credentials (workload identity,GOOGLE_APPLICATION_CREDENTIALS, or a gcloud user login).
A table carries only its own kind’s fields. A field belonging to a
different kind — vault_uri under kind = "asm", project under
kind = "akv" — is refused by name when the config loads, rather
than parsed and silently discarded. A half-finished migration between
secret managers therefore fails loudly at startup instead of quietly
reading from the source you believed you had left behind. The likeliest
shape is a stale environment override: {PREFIX}_..._KIND switched to a
new kind while the old kind’s ..._VAULT_URI (or ..._SECRET_ID) is
still exported, since those layer on top of the TOML. Each kind’s own
optional fields — asm’s region and endpoint_url, gsm’s version
— are unaffected.
A key belonging to no kind is refused the same way, naming it: key = { kind = "asm", secret_id = "…", regoin = "us-east-1" } fails with
table has an unknown field `regoin` . That is the message a plain
typo produces, and it is worth knowing which mistakes it now catches
that previously passed silently — a misspelled optional field, as
above, which used to leave the setting at its default, and a stray key
alongside an otherwise-valid table. A typo in a required field
always failed, but blamed the absence (requires `path` ) rather than
the cause; it now names the typo instead.
Two mistakes that used to escape by name no longer do. A kind whose
value is unknown fails with kind = "avk" is not a known kind; expected one of "file", "akv", "asm", "gsm" — the list quoted in the
message is the same list that accepts a spelling, so the two cannot
drift apart. A field of the right name but the wrong type names both the
field and what it found: path = 5 fails with table field `path` must be a string, found integer. Spellings stay exact and case-sensitive,
so kind = "AKV" is refused rather than quietly accepted.
Both used to fail while the table was still being parsed, where the
untagged form retried its other shapes and reported only “data did not
match any variant”. Neither the config that is accepted nor the config
that is refused has changed — only which of them tells you why. One
gap remains: a key that is neither a string nor a table (key = 5)
still reports the untagged message, because it matches no shape at all.
Every kind parses in every build. The clouds themselves are cargo
features of the key-source crate (akv / asm / gsm, all on by
default); a build that compiles one out still accepts the config but
fails at load with an error naming the feature to enable, so a slimmed
operator build can drop the SDKs it never uses (see
Slim-build features for per-binary
slim build commands).
(Cloudflare has no equivalent backend by design: its Secrets Store / Workers secrets are write-only over the API — only a deployed Worker binding can read a value — so a fetch-style key source cannot exist.)
There is no local Key Vault emulator, so the live AKV fetch is
exercised only by an #[ignore]d probe pointed at a real vault; the ASM
and GSM twins have the same shape, except that an ASM probe can target
LocalStack instead of the real cloud. Each probe runs only when its
environment variables are set, and the roster of those variables lives
in key_source/README.md’s Tests section (outside this book) rather
than being restated here — a test in that crate scans its sources and
fails if a gate variable is missing from that section, so the crate’s
list is the one that cannot fall behind. The per-kind dispatch is
unit-tested against a fake fetcher, so CI covers the dispatch and a real
cloud covers the round trip.
Cloud app-config sources: AWS AppConfig or Azure App Configuration
When mounting a TOML file into every container or VM is the awkward part
of a deployment — orchestrated fleets, serverless units, config that
several instances must share — a binary can pull its settings from a
managed configuration store instead: AWS AppConfig or Azure App
Configuration. The cloud tier slots into the resolution chain
above directly after the TOML file: it
overrides the file, and is itself overridden by ./.env,
./.env.$APP_ENV, and the real environment — so a {PREFIX}_{PATH}
override still beats a cloud value, exactly as it beats the file. This
tier carries settings, not secrets: key material keeps going through
key sources, and a cloud
config value holds at most a secret’s coordinates, never the secret.
Opting in is pure environment — no TOML key, no code change. Each binary
derives six bootstrap variables from its prefix (SITHBITD,
ACCOUNT_API, DOMAIN_SITHBIT, MAIL_GRPC, SITHBIT_IPFSD,
IPFS_GATEWAY):
| Variable | Meaning |
|---|---|
{PREFIX}_AWSAPPCONFIG | Select AWS AppConfig: application/environment/profile (exactly three non-empty segments) |
{PREFIX}_AWSAPPCONFIG_REGION | Optional region override (default: the ambient AWS configuration’s) |
{PREFIX}_AWSAPPCONFIG_ENDPOINT | Optional endpoint URL, for emulators/local fakes |
{PREFIX}_AZAPPCONFIG | Select Azure App Configuration: the store’s https://<name>.azconfig.io endpoint |
{PREFIX}_AZAPPCONFIG_LABEL | Optional label filter; unset reads the NULL label only, never every label |
{PREFIX}_AZAPPCONFIG_PREFIX | Optional key prefix, server-filtered and stripped before mapping (e.g. sithbitd:) |
With neither primary variable set the tier is skipped entirely — the
zero-config contract is untouched. Setting both is a load error
(pick one provider per binary). The bootstrap variables are control
inputs, not settings, and may themselves come from a .env file.
The payload idiom differs per provider:
- AWS AppConfig holds one whole TOML document in a freeform
configuration profile. It deep-merges over the config file: nested
tables merge key-wise, so one cloud document can override a section
without erasing its siblings; scalars and arrays replace wholesale.
Each load opens a fresh AppConfigData session and makes a single
GetLatestConfigurationcall — configuration is read once at startup, so picking up a new deployment means restarting the binary. - Azure App Configuration holds per-key values:
:in a key descends one TOML nesting level (store:kindsetsstore.kind), and values get the same TOML-scalar parsing as env overrides. Keys are case-sensitive — spell them exactly like the TOML keys. A scalar-vs-table collision between keys fails the load rather than letting listing order decide.
Both providers authenticate ambiently — the AWS credential chain, the
Azure managed identity — the same posture as the key sources above. The
backends are cargo features of the app-config crate (awsconf /
azconf, both on by default); a build that compiles one out fails at
load with an error naming the feature to rebuild with when its provider
is selected, so a slimmed operator build can drop the SDK it never uses
(see Slim-build features for
per-binary slim build commands).
The mapping logic (bootstrap parsing, TOML merge, key nesting) is
unit-tested against injected fake fetches, so CI covers it without
credentials. The live round trips are #[ignore]d probes — the AWS one
needs ambient AWS credentials and can be aimed at an emulator instead
of the real service, the Azure one needs an ambient managed identity —
and each runs only when its environment variables are set. The roster
of those variables lives in app_config/README.md’s Tests section
(outside this book) rather than being restated here — a test in that
crate scans its sources and fails if a gate variable is missing from
that section, so the crate’s list is the one that cannot fall behind.
You do not have to author the store content from scratch: the
repository ships ready-to-import production documents for both
providers — a complete six-service deployment shape with dummy secret
coordinates — under iac/appconfig/, together with the az appconfig kv import / aws appconfig runbooks and the generator that keeps the
two flavors in lock-step. See iac/README.md and the
deployment chapter.
Going public: which settings must change
Every per-binary page below marks the rows an operator has to revisit
before a listener leaves loopback. The same three-way legend applies on
every page, in the example TOMLs, and in the iac/appconfig/ production
documents:
- REQUIRED (public) — a reachable deployment is unsafe or
non-functional until the operator sets this deliberately: a public
bind address, the TLS certificate behind it, the hostname clients and
peers are told, a store shared across instances, a secret, or the
gateway’s mutual-TLS table once the gateway enforces
[auth]. - RECOMMENDED (public) — the in-code default is safe, but a public deployment should choose the value consciously: rate limits and budgets, blocklists, sender-authentication policy, quotas, telemetry.
- unmarked — the default is right as shipped; going public changes nothing.
A marker is an operator obligation, not a startup check. Two
binaries couple a non-loopback bind to a credential and refuse to start
without it — mail-grpc to its [auth] section, sithbit-ipfsd to
its auth_token — and no other binary validates its configuration
against the address it binds. Where a row’s setting is
enforced at startup for some other reason (a missing field, an empty
allow-list, a mismatched scheme) the row says so — otherwise assume a
misconfigured public instance starts cleanly and serves.
[health] and [observability] — every binary
Two sections shared by every TOML binary — mail-grpc included, since
it moved onto the same layering:
| Key | Default | Meaning |
|---|---|---|
health.bind_addr | 127.0.0.1:<per-binary port> | The /healthz + /readyz listener; each binary defaults its own port (8190–8198, table in Monitoring) |
health.enabled | true | Disable to serve no health endpoints (--health-probe then exits 1) |
observability.otlp | (absent — no export) | Presence of the section enables OTLP push of traces + metrics |
observability.otlp.endpoint | "http://127.0.0.1:4317" | Collector gRPC endpoint. The standard OTEL_EXPORTER_OTLP_*ENDPOINT env vars silently override this — leave them unset |
observability.otlp.metrics_interval_seconds | 60 | Metric push cadence |
Every binary also accepts a --health-probe argument: load the same
config, GET the health listener’s /readyz, exit 0/1 — this is what
the compose healthcheck: entries run inside the distroless images.
See Monitoring and backups for the endpoints, the
metric list, and the docker-compose.otel.yml collector overlay.
What’s on each page
The settings above — resolution order, key sources, cloud app-config sources, and the two sections every binary shares — apply across the whole reference. Each binary’s own settings are on its own page:
- sithbitd: core & chain settings —
what the daemon is,
[store],[grpc]/[ipfs] - sithbitd: SMTP & submission settings
—
[smtp],[submission], sender authentication, outbound quotas - sithbitd: IMAP, POP & security settings
—
[imap],[pop], login-attempt budgets, TLS posture, client-certificate auth - sithbitd: spooler settings —
[spooler], DMARC/TLS-RPT reporting, IPFS offload - account-api settings — the account API, static mounts, rate limits, sithbit-console
- domain-sithbit settings — the domain
verification service,
[mail_hosts],[mta_sts] - IPFS services settings —
sithbit-ipfsd, sithbit-gateway,
[swarm] - mail-grpc settings — the chain gateway
- Standalone protocol servers — pop-server, imap-server, smtp-server
- Which services get a
.env— a cross-cutting summary