Every environment variable the control plane reads is parsed in one place —
AppConfigService (src/config/app-config.service.ts). The only files allowed to
read process.env directly are main.ts, db/migrate.ts, telemetry/telemetry.ts,
the auth/{pinned-keys.service,pinned-keys-admin.controller,auth.module}.ts set,
and domain-packs/domain-packs.module.ts. Start from .env.example.
.env is loaded automatically at process start via a dotenv/config preload —
the first import in main.ts, before anything else runs — so it's populated
before AppConfigService is ever constructed (including the manual instance
main.ts uses to drive database migrations, ahead of Nest's own bootstrap).
Defaults below are the code defaults. Several variables are fail-fast in
production (NODE_ENV !== 'development') — see Startup validation.
Several keys are env-var equivalents of the registry's TOML config (e.g.
AUTH_REQUIRE_TENANT ↔ auth.require_tenant, TENANT_AGENTS ↔
[[auth.tenant_agents]], REVOCATION_FEEDS ↔ [[auth.revocation_feeds]],
CONTROL_PLANE_PINNED_KEYS ↔ [[playground.pinned_keys]]). They exist so the
CP enforces the same rules; the model behind them lives in the registry's
CONFIGURATION.md,
AUTHENTICATION.md,
and MULTI-TENANCY.md.
| Var | Type | Default | Meaning |
|---|
NODE_ENV | string | development | development relaxes fail-fast checks. |
PORT | number | 3001 | HTTP listen port. |
HOST | string | 0.0.0.0 | Bind address. |
PUBLIC_HOST | string | '' | Externally-resolvable host (example.com / example.com:8443) a consumer's did:web resolver hits for /.well-known/did.json. Distinct from HOST. Used to assert the did:web witness↔host binding at boot. |
CORS_ORIGIN | string | http://localhost:3000 | Allowed CORS origin. |
| Var | Type | Default | Meaning |
|---|
DATABASE_URL | string | postgres://postgres:postgres@localhost:5432/acdp_control_plane | Postgres connection string. |
DB_POOL_MAX | number | 20 | Max pool connections per replica. Must be ≥ 2. |
DB_POOL_IDLE_TIMEOUT | number (ms) | 30000 | Idle connection timeout. |
DB_POOL_CONNECTION_TIMEOUT | number (ms) | 5000 | Connection-acquisition timeout. |
See AUTH.md.
| Var | Type | Default | Meaning |
|---|
AUTH_API_KEYS | CSV | '' | Bearer API keys. Empty = auth bypassed (dev only). |
AUTH_ADMIN_API_KEYS | CSV | '' | Subset allowed admin ops (revoke any jti, reload pinned keys, read revocation feed, enroll registry, routing stats). |
AUTH_REQUIRE_TENANT | bool | false | Strict-tenant default-deny. See TENANCY.md. |
AUTH_PERSISTENCE | memory|postgres | memory | Backend for challenges/revocations/ledger. postgres required for multi-instance. |
AUTH_SWEEP_INTERVAL_SECONDS | number | 300 | Expired-state GC interval; ≤0 disables. |
TOKEN_ISSUANCE_ENABLED | bool | false | Enable /auth/challenge + /auth/token + JWT verify path. |
JWT_SECRET | string | '' | HS256 signing secret. ≥32 bytes when issuance + HS256. |
JWT_SIGNING_ALG | HS256|EdDSA | HS256 | Issuance algorithm. |
JWT_PRIVATE_KEY_PEM | string (PEM) | '' | Ed25519 PKCS8 private key. Required when issuance + EdDSA. |
JWT_KID | string | '' | Override kid; else derived from key fingerprint. |
JWT_AUTHORITY | string | control-plane.local | iss claim + challenge signing input. |
JWT_AUDIENCE | string | = JWT_AUTHORITY | aud claim bound + required on local verify. |
JWT_TTL_SECONDS | number | 3600 | Issued-token TTL. ≥60 when issuance. |
CHALLENGE_TTL_SECONDS | number | 300 | Challenge-nonce TTL. ≥30 when issuance. |
CONTROL_PLANE_PINNED_KEYS | CSV | '' | did=base64[:alg][:from..until]. Verification + emergency revocation. |
TRUSTED_ISSUERS | CSV | '' | Federated peers. iss|alg|material|audience[|scope]. audience required. |
REVOCATION_FEEDS | CSV | '' | Peer feeds to poll. issuer|url|admin_token[|poll_seconds]. |
See TENANCY.md.
| Var | Type | Default | Meaning |
|---|
TENANT_API_KEYS | CSV | '' | tenantId:key,…,bareKey. Bare keys → default. |
TENANT_AGENTS | CSV | '' | tenantId:agent_did,…. Stamps JWT tenant claim. |
TENANT_QUOTAS | string | '' | Per-tenant quotas. See POLICY.md. |
See POLICY.md.
| Var | Type | Default | Meaning |
|---|
POLICY_BACKEND | static|opa | static | Decision backend. |
OPA_URL | string | http://localhost:8181 | OPA sidecar base URL. |
OPA_PACKAGE_PATH | string | acdp/policy/v1 | OPA package path. |
OPA_TIMEOUT_MS | number | 1500 | Per-query timeout. |
OPA_FAIL_OPEN | bool | false | On OPA error, allow instead of deny. |
See INGEST.md.
| Var | Type | Default | Meaning |
|---|
WEBHOOK_SECRET | string | '' | Global HMAC secret for inbound webhooks. Empty = verification skipped (dev only — fails startup in production). |
INGEST_REQUIRE_ENROLLMENT | bool | false | Accept only enrolled authorities. |
INGEST_STRICT_TENANT | bool | false | Unenrolled authority may not assert non-default tenant. |
INGEST_MAX_BODY_BYTES | number | 1048576 | Raw body cap (1 MiB). |
INGEST_MAX_JSON_DEPTH | number | 64 | JSON nesting-depth cap. |
DOMAIN_PACKS | CSV | '' | Active domain packs (e.g. finance); gates custom context_types. |
| Var | Type | Default | Meaning |
|---|
WEBHOOK_RETRY_INTERVAL_MS | number | 300000 | Outbox retry-sweep interval; ≤0 disables. |
WEBHOOK_SSRF_ALLOW_HTTP | bool | false | Allow non-HTTPS subscriber URLs (dev only). |
WEBHOOK_SSRF_ALLOW_LOOPBACK | bool | false | Allow loopback/localhost subscriber URLs (dev only). |
| Var | Type | Default | Meaning |
|---|
STREAM_HUB_STRATEGY | memory|redis | memory | SSE fan-out backend. redis for multi-instance. |
REDIS_URL | string | '' | Redis connection (SSE redis strategy + Redis quota store). |
STREAM_SSE_HEARTBEAT_MS | number | 15000 | SSE heartbeat interval. |
| Var | Type | Default | Meaning |
|---|
THROTTLE_TTL_MS | number | 60000 | Throttle window per (actorId|ip). |
THROTTLE_LIMIT | number | 200 | Requests per window. /auth/* uses a tighter override. |
An advisory-locked sweep that makes the CP an independent second observer of
registry receipts: it picks unaudited context_published events, cross-checks
each embedded registry_receipt, records a verdict in receipt_audits, and
surfaces it as the trust member on GET /runs/:runId. The receipt format and
the verification procedure it runs are normative in
RFC-ACDP-0010
(registry-side runbook:
acdp-registry-rs/docs/RECEIPTS.md) —
see ARCHITECTURE.md
for how the sweep fits the pipeline.
| Var | Type | Default | Meaning |
|---|
RECEIPT_AUDIT_ENABLED | bool | false | Enable the receipt-audit sweep. |
RECEIPT_AUDIT_INTERVAL_SECONDS | number | 300 | Sweep interval. ≥5 when enabled. |
RECEIPT_AUDIT_BATCH_SIZE | number | 50 | Events audited per sweep. ≥1 when enabled. |
RECEIPT_AUDIT_LOOKBACK_HOURS | number | 24 | Only events younger than this are picked up. |
The checkpoint witness polls GET /log/checkpoint on enrolled registries advertising
acdp-registry-transparency-log, verifies each checkpoint's signature and its
consistency against the last-witnessed head, and alerts on any dishonesty signal
(root rewrite, split view, tree-size regression, log reset). The checkpoint/proof
formats and checks are normative in
RFC-ACDP-0012
and RFC-ACDP-0015;
the knobs below are what this service exposes.
| Var | Type | Default | Meaning |
|---|
LOG_WITNESS_ENABLED | bool | false | Enable the checkpoint-witness sweep. |
LOG_WITNESS_INTERVAL_SECONDS | number | 300 | Sweep interval. ≥5 when enabled. |
LOG_WITNESS_EXCLUDE_AUTHORITIES | list | '' | Authorities never witnessed. |
Witness cosigning (RFC-ACDP-0015). When enabled, a checkpoint that passes the §7
obligation (signature valid and consistency from the retained head) is cosigned:
the witness mints a signed acdp-log-cosignature over the observed tuple with its own
Ed25519 assertionMethod key and serves it at GET /log/witness. Riding the checkpoint
witness, it requires LOG_WITNESS_ENABLED=true. A checkpoint that fails the
obligation is never cosigned.
| Var | Type | Default | Meaning |
|---|
WITNESS_COSIGNING_ENABLED | bool | false | Enable minting + serving witness cosignatures. |
WITNESS_ID | string | '' | The witness's DID (did:web:<this-CP-host> or did:key). Required when enabled. |
WITNESS_SIGNING_PRIVATE_KEY_PEM | string | '' | PEM-encoded Ed25519 private key the witness cosigns with. Required when enabled. Dedicated — never the JWT IdP key (§5/§15). |
WITNESS_KEY_ID | string | '' | assertionMethod key id (DID URL under WITNESS_ID). Defaults to <WITNESS_ID>#witness-key-1. |
Generate the witness key with openssl genpkey -algorithm ed25519. When WITNESS_ID
is a did:web, its host must match PUBLIC_HOST so consumers can
dereference /.well-known/did.json on this CP — the binding is asserted at boot
(RFC-ACDP-0015 §9): a mismatch is fatal, and an unset PUBLIC_HOST only warns. did:key
witnesses are exempt (self-describing).
Witness quorum consumption (RFC-ACDP-0015 §8). The mirror of cosigning: instead of
minting, evaluate the N-witnessed quorum over the cosignatures a registry aggregates
and serves on GET /log/checkpoint (the top-level witness_signatures sibling, §6.1).
Each is verified against its witness's own resolved did:web document, and DISTINCT
trusted witnesses over the checkpoint's exact (log_id, tree_size, root_hash) tuple are
counted — never the CP's own local mint. The count + meets_quorum are recorded on the
witnessed head (surfaced on GET /registries/:authority/log-witness per checkpoint and on
the dashboard logWitness.headsMeetingQuorum tile). Rides the checkpoint witness, so it
requires LOG_WITNESS_ENABLED=true.
| Var | Type | Default | Meaning |
|---|
WITNESS_QUORUM_ENABLED | bool | false | Enable quorum consumption over aggregated cosignatures. |
WITNESS_QUORUM_TRUSTED | list | '' | Witness DIDs whose cosignatures count; others are verified-but-ignored. |
WITNESS_QUORUM_MIN_WITNESSES | number | 1 | The N in N-witnessed. ≥1 when enabled. |
Log-inclusion audit (RFC-ACDP-0012).
The sibling sweep to the checkpoint witness: for stored receipt-bearing publishes
from log-advertising registries it proves each context is actually in the
registry's log (fetching /log/proof?ctx_id=) and cross-binds against witnessed
heads. Verdicts (included | invalid_proof | not_logged | no_log | error)
seal once per event in log_inclusion_audits.
| Var | Type | Default | Meaning |
|---|
LOG_INCLUSION_AUDIT_ENABLED | bool | false | Enable the inclusion-audit sweep. |
LOG_INCLUSION_AUDIT_INTERVAL_SECONDS | number | 300 | Sweep interval. ≥5 when enabled. |
LOG_INCLUSION_AUDIT_BATCH_SIZE | number | 50 | Events audited per sweep. ≥1 when enabled. |
LOG_INCLUSION_AUDIT_LOOKBACK_HOURS | number | 24 | Only events younger than this are picked up. |
| Var | Type | Default | Meaning |
|---|
DATA_RETENTION_ENABLED | bool | false | Enable periodic purge of aged rows. |
DATA_RETENTION_TTL_DAYS | number | 30 | Age threshold. ≥1 when enabled. |
DATA_RETENTION_INTERVAL_HOURS | number | 24 | Purge-job interval. |
| Var | Type | Default | Meaning |
|---|
BANDIT_EXPLORATION_FRACTION | number | 0.05 | Fraction of traffic using uniform exploration vs Thompson sampling. |
| Var | Type | Default | Meaning |
|---|
LOG_LEVEL | string | info | debug|info|warn|error. |
OTEL_ENABLED | bool | false | Enable OpenTelemetry SDK. |
OTEL_SERVICE_NAME | string | acdp-control-plane | Span/metric service name. |
OTEL_EXPORTER_OTLP_ENDPOINT | string | '' | OTLP endpoint; empty discards traces. |
SWAGGER_ENABLED | bool | dev: on / prod: off | Serve Swagger UI. |
SWAGGER_PATH | string | docs | Swagger UI path. |
| Var | Type | Default | Meaning |
|---|
PLAYGROUND_URL | string | '' | Playground backend for run-completion notifications; empty disables. |
AppConfigService.validate() runs at boot. Throws (refuses to start) on:
- Tenant bindings configured (
TENANT_AGENTS or tenant-bound TENANT_API_KEYS)
without AUTH_REQUIRE_TENANT=true.
- Production with empty
AUTH_API_KEYS.
- Production with empty
WEBHOOK_SECRET (inbound webhook HMAC verification would
otherwise be silently disabled — see src/ingest/hmac.ts).
DB_POOL_MAX < 2.
DATA_RETENTION_ENABLED=true with DATA_RETENTION_TTL_DAYS < 1.
POLICY_BACKEND not in opa; JWT_SIGNING_ALG not in EdDSA.
- Issuance +
HS256 with JWT_SECRET < 32 bytes.
- Issuance +
EdDSA with empty JWT_PRIVATE_KEY_PEM.
- Issuance with
JWT_TTL_SECONDS < 60 or CHALLENGE_TTL_SECONDS < 30.
LOG_WITNESS_ENABLED=true with LOG_WITNESS_INTERVAL_SECONDS < 5.
RECEIPT_AUDIT_ENABLED=true with RECEIPT_AUDIT_INTERVAL_SECONDS < 5 or
RECEIPT_AUDIT_BATCH_SIZE < 1.
LOG_INCLUSION_AUDIT_ENABLED=true with LOG_INCLUSION_AUDIT_INTERVAL_SECONDS < 5
or LOG_INCLUSION_AUDIT_BATCH_SIZE < 1.
WITNESS_COSIGNING_ENABLED=true without WITNESS_ID, without
WITNESS_SIGNING_PRIVATE_KEY_PEM, or without LOG_WITNESS_ENABLED=true.
(WitnessSigningService additionally rejects a non-Ed25519 key, a malformed
witness DID, or a WITNESS_KEY_ID not under WITNESS_ID — in every environment.)
Warns (starts, but flags a risk) on, in production:
STREAM_HUB_STRATEGY=memory (SSE won't sync across replicas).
OTEL_ENABLED=true with empty OTEL_EXPORTER_OTLP_ENDPOINT (traces discarded).
REVOCATION_FEEDS set with TOKEN_ISSUANCE_ENABLED=false (poller won't run).
TOKEN_ISSUANCE_ENABLED=true with AUTH_PERSISTENCE=memory (state not shared).
WITNESS_QUORUM_ENABLED=true with empty WITNESS_QUORUM_TRUSTED (no
cosignature can ever count toward quorum).