Configuration

acdp-registry is configured by a TOML file layered with environment-variable overrides. There is no clap CLI — config loading is the config crate plus env vars (same dependency-minimization principle as acdp-rs). The schema is RegistryConfig in crates/acdp-registry-types/src/config.rs; a worked example is config/registry.example.toml.

Loading and precedence

built-in defaults  <  TOML file  <  ACDP_REGISTRY_* env vars
  • The TOML file path comes from ACDP_REGISTRY_CONFIG; when unset the binary falls back to its defaults (dev runs use SQLite under ./data/registry.db).

  • Env overrides use ACDP_REGISTRY_<SECTION>__<FIELD> — a single underscore after the ACDP_REGISTRY prefix, then double underscores between nesting levels:

    export ACDP_REGISTRY_STORAGE__POSTGRES_URL="postgres://acdp:acdp@db:5432/acdp"
    export ACDP_REGISTRY_AUTH__JWT_SECRET="$(openssl rand -base64 32)"
    export ACDP_REGISTRY_AUTH__JWT_SIGNING_ALG="EdDSA"

Startup validation

The binary validates config before serving and refuses to boot on a misconfig (validate_config in crates/acdp-registry-server/src/main.rs). It enforces:

  • Auth — jwt_signing_alg ∈ EdDSA. EdDSA requires a non-empty jwt_private_key_pem. HS256 with an empty jwt_secret requires allow_ephemeral_secret = true, otherwise it fails. A non-empty secret is rejected if it is the literal changeme, and must decode to ≥32 bytes.
  • Webhook — when enabled, url must be non-empty and pass the SSRF policy (HTTPS, no private/internal authorities), and secret must be non-empty.
  • Multi-tenancy — a non-empty [[auth.tenant_agents]] requires require_tenant = true (you can't half-enable tenancy).
  • Bind safety — a non-loopback bind with neither TLS nor auth requires an explicit allow_public_bind = true.
  • TLS — when tls.enabled, cert_path and key_path must exist on disk.
  • DID methods — auth.did_methods entries must be did:web or did:key, and did:web must be present (RFC-ACDP-0007 §3.1).
  • Receipts — a configured [receipt] key must parse (exactly one source, valid base64, 32 bytes), and is incompatible with playground.enabled (RFC-ACDP-0010 §7: a receipts registry has no unverified publish path).
  • Profile allowlist (REG-5) — every entry in registry.profiles must be one of the seven registry profiles the pinned ACDP spec defines (REGISTRY_ADVERTISABLE_PROFILES in crates/acdp-registry-types/src/ config.rs: acdp-registry-core, acdp-registry-discovery, acdp-registry-federated, acdp-registry-receipts, acdp-registry-head-receipts, acdp-registry-transparency-log, acdp-registry-lifecycle). This is checked before any other registry.profiles-dependent guard below, so a typo or an out-of-scope profile is reported first. acdp-log-witness is explicitly rejected with a dedicated message: a witness is not a registry (RFC-ACDP-0015 §6.1) — a registry MAY aggregate cosignatures under acdp-registry-transparency-log without ever advertising acdp-log-witness itself.
  • 0.3.0 profiles — receipt.head_receipts = true requires a configured [receipt] signing key (RFC-ACDP-0011 §9: head receipts are signed with the receipt key). log.enabled = true likewise requires a [receipt] key (RFC-ACDP-0012 §11: leaves bind receipt hashes and checkpoints sign with the receipt key), a durable storage backend (SQLite/Postgres — the memory backend cannot honor the append-only history commitment), and a well-formed log.instance ([a-z0-9-]{1,32}). Listing acdp-registry-head-receipts, acdp-registry-lifecycle, or acdp-registry-transparency-log in registry.profiles without enabling the matching feature is refused as a false capability advertisement.
  • Witnesses (0.4.0) — [[witnesses]] requires log.enabled = true (RFC-ACDP-0015 §6.1: there are no checkpoints to witness without a log); each did must be a did:web DID and each url must pass the SSRF policy (HTTPS, non-private host).
  • Rate limiting (FEAT-06) — every rate_limit.trusted_proxies entry must be a valid CIDR (or bare IP), so a typo fails the boot rather than silently disabling XFF trust. A non-empty trusted_proxies with rate_limit.enabled = false is refused (XFF would be parsed for nothing).
  • Metrics (FEAT-10) — when metrics.enabled, duration_buckets must be non-empty, positive, finite, and strictly increasing (Prometheus histogram bounds).

Reference

Defaults below are the struct defaults; the example TOML may set different values for illustration. Env var = ACDP_REGISTRY_ + the bracketed path.

[registry]

KeyTypeDefaultNotes
authoritystring—Bare lowercase DNS name. Mints ctx_id and is the did:web registry id.
portu168443Listen port.
bindstring127.0.0.1Bind address. Non-loopback needs TLS/auth or allow_public_bind.
allow_public_bindboolfalseOpt-in to bind a public interface without TLS/auth.
base_urlstringhttps://{authority}Public URL advertised to consumers / federation control plane.
profilesstring[]["acdp-registry-core","acdp-registry-discovery"]Advertised in capabilities. Must be a subset of the seven acdp-registry-* profiles the pinned spec defines — any other value (including acdp-log-witness, which is not a registry profile) fails startup.
cross_registry_resolutionbooltrueForward foreign ctx_ids to their home registry; false returns 404 instead.

[registry.tls]

KeyTypeDefaultNotes
enabledboolfalseServe HTTPS directly via rustls. Usually terminate TLS upstream instead.
cert_pathpath—Required when enabled.
key_pathpath—Required when enabled.

[registry.cors]

KeyTypeDefaultNotes
allowed_originsstring[][]Empty disables CORS (no headers sent). List your UI origin(s) to opt in.

[storage]

KeyTypeDefaultNotes
backendenumsqlitepostgres | sqlite | memory. Must match the compiled storage feature.
postgres_urlstring—Required when backend = "postgres".
sqlite_pathpath./data/registry.dbSQLite file.
max_connectionsu3220sqlx pool size.

The storage backend is also chosen at compile time via the acdp-registry-server Cargo features (storage-sqlite default, storage-pg, storage-memory). The backend config key must agree with the built binary.

[auth]

KeyTypeDefaultNotes
enabledboolfalseMounts /auth/* and turns on the bearer/visibility gates.
did_methodsstring[]["did:web"]Allowed DID methods; advertised in capabilities.
jwt_signing_algstringHS256HS256 or EdDSA. See AUTHENTICATION.md.
jwt_secretstring""HS256 secret — base64, ≥32 bytes. Never published.
allow_ephemeral_secretboolfalseHS256 dev escape hatch: random process-lifetime key when jwt_secret is empty.
jwt_private_key_pemstring""EdDSA Ed25519 private key (PKCS#8 PEM). Required for EdDSA.
jwt_kidstring""Optional explicit JWKS key id; default is the public-key fingerprint.
token_ttl_secondsu643600JWT lifetime.
challenge_ttl_secondsu64300Challenge nonce validity.
token_leeway_secondsu6430Clock-skew tolerance for exp.
anonymous_public_readsboolfalseAllow unauthenticated reads of public contexts. Opt in for discovery hubs.
require_tenantboolfalseStrict multi-tenancy: requests resolving to no tenant are denied. See MULTI-TENANCY.md.
admin_tokensstring[][]Bearer tokens for /admin/*. Empty disables every admin-bearer-gated route: /admin/status, /admin/lineages/{id}/audit, /admin/contexts/{id}/retract, /admin/contexts/{id}/republish, GET /admin/contexts, /admin/pinned-keys/reload. See HTTP-API.md#admin.

[[auth.tenant_agents]]

Repeatable. Binds a producing/consuming agent to a tenant.

KeyTypeNotes
agent_didstringFull DID of the agent.
tenant_idstringTenant the agent is scoped to (default is reserved).

[[auth.revocation_feeds]]

Repeatable. A peer registry whose revocations this registry mirrors.

KeyTypeDefaultNotes
issuerstring—Peer DID; each fetched entry's iss must match.
feed_urlstring—Peer's /auth/revocations URL.
admin_tokenstring—Bearer for the peer's feed.
poll_secondsu64300Poll interval.

[webhook]

KeyTypeDefaultNotes
enabledboolfalse
urlstring""HMAC-signed POST target. SSRF-policy gated.
secretstring""HMAC-SHA256 key; must be non-empty when enabled.
timeout_secondsu645Per-delivery timeout.
max_retriesu323Exponential backoff (250 ms → cap 15 s).
queue_capacityusize1024Bounded in-memory queue; events drop (with a warn) when full.

See WEBHOOKS.md.

[limits]

KeyTypeDefaultNotes
max_payload_bytesu641048576Publish body cap (enforced by the body-limit layer on every route).
max_embedded_bytesu6465536Cap on an inline data_ref value.
idempotency_key_ttl_secondsu6486400Idempotency-Key replay window; advertised in capabilities.
publish_rate_per_minuteu3260Per-agent POST /contexts cap; 0 disables. In-memory, per-process.
challenge_rate_per_minuteu3260Per-agent POST /auth/challenge cap; 0 disables. In-memory, per-process.

These are per-process in-memory token buckets — see OPERATIONS.md · Rate limiting for the multi-replica caveat.

[rate_limit] (FEAT-06)

Per-IP and process-global rate limiting on the /auth/* endpoints (token issuance / refresh / revoke), applied as middleware over the whole /auth/* subrouter — on top of the per-agent [limits] budgets. The [limits] buckets key on the caller-supplied agent_id, which an unauthenticated attacker controls and can rotate to defeat the per-key limit; these two bounds are attacker-independent.

KeyTypeDefaultNotes
enabledbooltrueMaster switch. On by default — /auth/* is the most attacker-controllable surface.
per_ip_per_minuteu3260Per-resolved-client-IP cap on /auth/*; 0 disables the per-IP bound.
global_per_minuteu326000Whole-process ceiling across all IPs; 0 disables it. Bounds a source-IP-rotating flood.
trusted_proxieslist<CIDR>[]Reverse-proxy CIDRs whose X-Forwarded-For is trusted. Empty = never trust XFF.

Client-IP resolution & the trusted-proxy decision (security). The client IP defaults to the TCP socket peer. X-Forwarded-For is caller-supplied and is never trusted unless the socket peer is itself in one of the trusted_proxies ranges — otherwise any client could spoof its source IP to evade the per-IP budget or frame another address. When the peer is a trusted proxy, the real client is taken from the rightmost X-Forwarded-For entry that is not itself a trusted proxy (walking a chain of trusted hops from the right). List only proxies you operate; a wrong entry is a spoofing hole. CIDRs are validated at startup — a malformed entry fails the boot rather than silently disabling XFF trust.

Same per-process, in-memory caveat as [limits]. Behind a load balancer with trusted_proxies set, each replica limits per real client IP; the global_per_minute ceiling is per replica. Requests are admitted or rejected before any DID resolution, so the SSRF/DNS path never runs for a throttled request.

[metrics] (FEAT-10)

Prometheus /metrics endpoint (text exposition, version=0.0.4). Off by default. When enabled, GET /metrics is mounted outside the ACDP auth pipeline and the rate limiter so a scraper reaches it unimpeded, and a process-global recorder captures HTTP request metrics plus domain counters.

KeyTypeDefaultNotes
enabledboolfalseMount /metrics and start recording.
bearer_tokenstring""When set, /metrics requires Authorization: Bearer <token>. Empty = open.
duration_bucketslist<f64>web-latency ladderBuckets (seconds) for the request-latency histogram; must be positive and strictly increasing.

See HTTP-API.md · GET /metrics for the exposed metric names.

[playground]

Never enable in production — setting enabled = true skips DID-signature verification on publishes from non-did:key agents, regardless of which Cargo features the binary was built with. (did:key publishes are verified before the playground branch runs and are unaffected — see below — and a pinned agent under [[playground.pinned_keys]] is still cryptographically verified even with enabled = true; the bypass applies to unpinned, non-did:key publishes.)

Only the two admin routes this feature unlocks (GET /admin/contexts, POST /admin/pinned-keys/reload) are compiled in with the playground Cargo feature — see OPERATIONS.md and HTTP-API.md. [playground] enabled = true itself is not feature-gated: the publish handler's DID-signature bypass (crates/acdp-registry-core/src/handlers/context.rs, the playground_snapshot.enabled branch) is a plain runtime if, compiled into every build including a stock release binary with default features. did:key producers are checked before this branch, unconditionally, through acdp's offline verifier (context.rs:414, comment at :423-432) — a did:key identity is self-verifying by construction, so [playground] never affects how a did:key publish is authorized. Pinned agents ([[playground.pinned_keys]]) are cryptographically verified inside the playground branch itself (context.rs:457-464); the skip applies only to publishes from non-did:key agents that aren't pinned.

KeyTypeDefaultNotes
enabledboolfalseSkip DID-signature verification for hands-on demos.
pinned_onlyboolfalseReject publishes from agents without a pinned key.

[[playground.pinned_keys]]

Repeatable.

KeyTypeDefaultNotes
agent_didstring—Full DID.
public_key_b64string—Standard base64 of the raw 32-byte key.
algorithmstringed25519Only ed25519 today.
valid_fromi64—Unix seconds, inclusive; open-ended if omitted.
valid_untili64—Unix seconds, exclusive; open-ended if omitted.

Hot-reload the [playground] section with POST /admin/pinned-keys/reload (playground feature) — see HTTP-API.md.

[receipt] (ACDP 0.2.0)

Registry-receipt signing identity (RFC-ACDP-0010). Configuring a key enables receipt minting, the acdp-registry-receipts profile, and GET /.well-known/did.json. Leave unset to stay a receipt-less registry. (acdp_version itself is unconditionally "0.5.0" as of REG-3 — RFC-ACDP-0016 §10's anchors claim always wins the version max() — so it no longer moves with [receipt]; the acdp-registry-receipts profile is what actually signals receipt minting is active.) See RECEIPTS.md for the operator runbook (rotation, retention, backfill policy).

KeyTypeDefaultNotes
signing_key_seed_b64string""Standard base64 of the raw 32-byte Ed25519 seed. Exactly one of the two key sources may be set.
signing_key_pathpath—File (e.g. mounted secret) whose contents are that base64 string.
key_id_fragmentstringreceipt-key-1Fragment under the registry DID; signature.key_id = did:web:<authority>#<fragment>. Pick a fresh fragment per rotation.
head_receiptsboolfalse(ACDP 0.3.0) Mint a lineage-head receipt on every GET /lineages/{id}/current response (RFC-ACDP-0011). Requires a configured signing key (the same receipt key signs — no new key role); advertises acdp-registry-head-receipts (acdp_version itself is unconditionally "0.5.0" as of REG-3 and no longer moves with this flag).

[[receipt.retired_keys]]

Repeatable. Rotated-out receipt keys, published in the DID document's verificationMethod only (never assertionMethod). Removing an entry bricks every receipt that key signed — RFC-ACDP-0010 §9 retains retired keys indefinitely; remove only on confirmed compromise.

KeyTypeDefaultNotes
public_key_b64string—Standard base64 of the raw 32-byte Ed25519 public key.
key_id_fragmentstring—The fragment the key was published under while active.

[lifecycle] (ACDP 0.3.0)

Lifecycle events & retraction (RFC-ACDP-0013). When enabled the registry serves POST /contexts/{ctx_id}/retract / /republish, derives status with the retracted > superseded > expired > active precedence, excludes retracted contexts from default search and from /current, serves registry_state.lifecycle_events, and advertises acdp-registry-lifecycle (acdp_version itself is unconditionally "0.5.0" as of REG-3 — RFC-ACDP-0016 §10's anchors claim always wins the version max() — and no longer moves with this flag). When disabled (the default) both endpoints answer 501 not_implemented and neither lifecycle_events nor the retracted status is ever emitted.

KeyTypeDefaultNotes
enabledboolfalseOpt into the RFC-ACDP-0013 endpoint surface and status semantics.

enabled also unlocks the registry-attested takedown path (POST /admin/contexts/{ctx_id}/{retract,republish}, RFC-ACDP-0013 §6 registry-initiated events — see HTTP-API.md). That path adds no new knob: it is gated by the existing auth.admin_tokens bearer, and its signing is driven by whether a [receipt] key is configured — with a receipt key the registry MUST sign the event under it; without one the event is recorded unsigned but still attributed to the registry DID.

[log] (ACDP 0.3.0)

Registry transparency log (RFC-ACDP-0012): a per-registry, append-only RFC 6962-style Merkle tree over publish events. When enabled the registry appends one leaf per accepted publish in the same storage transaction as the context row and its receipt (§7.1 — the body, the receipt, and the leaf commit together, or none does; a publish that cannot durably append its leaf fails), serves GET /log/checkpoint, GET /log/proof, and GET /log/entries, signs checkpoints with the [receipt] key (§6: no new key role), and advertises acdp-registry-transparency-log (acdp_version itself is unconditionally "0.5.0" as of REG-3 and no longer moves with this flag). When disabled (the default) the three /log/* endpoints answer 501 not_implemented. There is no degraded mode and no log_unavailable error.

Prerequisites (enforced at startup): a configured [receipt] signing key and a durable storage backend (sqlite or postgres).

Storage: leaves live in the log_leaves table — dense 0-based leaf_index in acceptance order, one leaf per ctx_id, and the exact JCS-canonical leaf bytes plus their sha256: leaf hash, so every leaf is byte-exactly reproducible forever. Roots and proofs are recomputed per request from the ordered leaf hashes (O(n); the head root is cached). Contexts published before enablement are not backfilled automatically; per §7.3 their history would be time-unanchored anyway.

KeyTypeDefaultNotes
enabledboolfalseOpt into the RFC-ACDP-0012 log: atomic leaf appends + the three /log/* endpoints.
instancestring"1"The <instance> component of log_id = did:web:<authority>/log/<instance> (matches [a-z0-9-]{1,32}). Change only on catastrophic tree loss (§7.4) — a new instance is an explicit, loudly detectable history reset.

[[witnesses]] (ACDP 0.4.0)

Transparency-log witness cosignature aggregation (RFC-ACDP-0015 §6.1). An array of independent witnesses this registry polls and whose verified cosignatures it attaches to its checkpoint responses (the reserved top-level witness_signatures member — see HTTP-API.md). A consumer then gets a checkpoint and its witness quorum in one fetch and verifies N-witnessed locally.

For each configured witness a background poller GETs <url>?log_id=<this registry's log_id> over the SSRF-guarded outbound client (HTTPS-only, DNS-rebinding-guarded, no redirects — RFC-ACDP-0008 §4.8), and for every returned cosignature runs the RFC-ACDP-0015 §8 verification procedure against this registry's own checkpoint at that tree_size: closed parse, the witness signature under the witness DID's assertionMethod key (resolved via the did:web resolver), and — the load-bearing check — that the cosignature's witnessed_checkpoint matches this registry's own root at that size. A witness cosigning a different root (a fork, or a lie) is logged and dropped — it is never stored and never served. Only verified cosignatures are persisted (table log_witness_cosignatures, keyed by (log_id, tree_size, root_hash, witness_did); a fresh re-observation upserts, newest wins), so serving is a single indexed read with no blocking network call in the request path.

Aggregation is a pure convenience: the registry never holds a witness key, so it can neither forge a cosignature nor make itself a trust dependency — a consumer MAY always fetch direct from a witness (§6.2). There is no new capability flag or profile: a registry that aggregates does so under its existing acdp-registry-transparency-log profile (§10). acdp_version served at .well-known/acdp.json is unconditionally "0.5.0" as of REG-3 (RFC-ACDP-0016 §10's anchors claim always wins the version max()), so configuring [[witnesses]] no longer moves it; whether aggregation is active is visible in the response body (whether witness_signatures accompanies a checkpoint) — see HTTP-API.md.

Prerequisites (enforced at startup): log.enabled = true (there are no checkpoints to witness without a log), each did a did:web DID, and each url accepted by the SSRF policy (HTTPS, non-private host). Empty (the default) disables aggregation entirely.

[[witnesses]]
did          = "did:web:witness.example.org"
url          = "https://witness.example.org/log/witness"
poll_seconds = 60                              # default 300

[[witnesses]]
did = "did:web:witness-2.example.org"
url = "https://witness-2.example.org/log/witness"
KeyTypeDefaultNotes
didstring(required)The witness's did:web DID. Cosignatures whose witness_id ≠ this value are ignored (a witness endpoint only speaks for its own DID).
urlstring(required)HTTPS URL of the witness's GET /log/witness endpoint (RFC-ACDP-0015 §6.2). SSRF-checked at startup and at DNS time on every poll.
poll_secondsinteger300Poll interval.