Configuration Reference
On this page
- Overview
- Common Variables (all services)
- Operator Tooling Variables (xtask)
- Security Variables
- Service-Specific Variables
- canopy-applications
- canopy-notices
- canopy-snap
- Token-exchange broker (OIDC A1, #1424)
- Receiver contract (OIDC S-slices, #1425+)
- canopy-security (chain-v2 append transport, #1207)
- canopy-security (chain-v2 verifiers, #1205 — plan D11)
- canopy-security (audit archival, #1208)
- canopy-tanf
- canopy-medicaid
- canopy-reporting
- canopy-web (Worker Portal BFF)
- Docker Compose Variables
- Port Map
Overview
All Canopy services are configured via environment variables following the convention CANOPY_{SERVICE}__{KEY}. The double underscore separates the service prefix from the configuration key.
Settings are loaded at startup by canopy-common::settings::ServiceSettings::load(prefix) using the config crate.
Common Variables (all services)
These apply to every service unless noted otherwise.
| Variable | Type | Default | Description |
|---|---|---|---|
|
u16 |
Required |
HTTP listen port |
|
String |
Required |
PostgreSQL connection string. Use |
|
String |
Required |
RabbitMQ AMQP URL. Use |
|
String |
Required |
Public Keycloak issuer URL (appears in JWT |
|
String |
Falls back to KEYCLOAK_ISSUER |
Internal URL for JWKS fetching (different from issuer in Docker deployments) |
|
String |
|
Logging level (trace, debug, info, warn, error) |
|
String |
Required |
Jurisdiction identifier for rulesets (e.g., |
|
String |
Comma-separated CORS origins or |
|
|
usize |
2097152 (2 MiB) |
Max request body size in bytes |
|
u32 |
10 |
Database connection pool size |
|
u64 |
600 |
Idle connection timeout in seconds |
|
u64 |
6000 |
Rate limit per IP per minute (0 = disabled) |
|
u64 |
0 (service default) |
Session TTL override for canopy-web (worker BFF) only; default 28800 (8h). canopy-portal does not expose a |
|
bool |
|
#1310 accountable override (ADR-041 pattern) for the case-detail confidentiality gate. Default off = fail CLOSED: when the household-confidentiality lookup (newest application, #1146) fails, every case-detail address surface withholds the street ("Address withheld") and the hero badge escalates to the attention-drawing |
|
u64 |
|
#1306 aggregate SSR deadline budget for full-page renders (plan |
|
u64 |
|
#1306 aggregate SSR deadline budget for htmx fragments (tab loads, panel retries, |
|
String (optional) |
unset |
Redis URL backing the replica-aware per-IP rate limiter (#1227): counters are a shared atomic fixed window, so the effective limit is invariant under replica count. Set for the internet-facing BFF edge (canopy-web in the devstack); unset — or on Redis failure — the service degrades to the process-local limiter (replica-diluted, never unlimited). The whole per-request check is additionally bounded by a fixed 250ms budget (#1287): a SLOW Redis (fsync pause, mid-reconnect) degrades to the same fallback instead of stalling the edge — the budget trades limiter precision, never availability. Consumed only by binaries built with canopy-api’s |
|
String |
|
Rulesets root the applicant portal reads its jurisdiction config from at boot (#1226). The container image sets |
|
String |
|
Jurisdiction whose |
|
u64 |
|
#1367 idle/error sleep between a notice worker loop’s claim passes. Dormant default = the historical 2 s literal (config-absent behavior byte-identical, pinned by config tests); the devstack sets 200 ms so e2e notice flows aren’t cadence-bound. Domain 50..=60000 ms, validated at boot — error, never clamp. |
|
u64 |
|
#1367 — the same knob for the notice dispatcher (delivery) loop. Devstack: 200 ms. Same domain and boot posture as |
|
u64 |
|
#1367 drain cadence of the #1105 CB assessment worker (the money path the cross-service suite awaits). Dormant default = the historical 5 s constant; devstack 500 ms. Domain 50..=60000 ms, boot-validated. |
|
u64 |
|
#1367 tick of the #1138 issuance-settlement loop. Dormant default = the historical 5 s constant; devstack 500 ms. The ~hourly aged-pending alert cadence is derived from the tick, so tuning the tick never changes alert frequency. Domain 50..=60000 ms, boot-validated. |
|
i32 |
10 |
Row-culpable publish attempts before an outbox row PARKS (#1230): the drainer stops retrying it (ending the poison-row log flood), the parked count degrades the |
|
i64 |
10000 |
Pending (unpublished, unparked) outbox rows above which the |
|
i64 |
900 |
Age bound for the oldest unpublished outbox row before the |
|
String |
|
Runtime environment. |
|
bool |
|
When true, bootstrap runs NO migrator — migrations are owned by the deploy-time job ( |
|
bool |
|
Set ONLY by deployments where the migration job is wired (the devstack |
Operator Tooling Variables (xtask)
Consumed by host-side operator commands, never by services. URLs travel via the environment, never argv (process listings / shell history).
| Variable | Default | Description |
|---|---|---|
|
devstack per-service default |
The service database |
|
devstack canopy_security default |
The anchor database for the genesis manifest. The command hard-compares the database name to canopy_security INDEPENDENT of |
|
devstack per-service default |
The database |
|
— (required for |
|
|
|
Exchanger client for the #1501 sweep-tool token exchange. The defaults are the public-by-design devstack pair (Kerckhoffs); production sets both to its provisioned exchanger client. |
|
|
Keycloak issuer for the #1501 sweep-tool exchange ( |
|
|
Exchanger client for |
Security Variables
| Variable | Default | Description |
|---|---|---|
|
Required |
AES-256-GCM base64-encoded 32-byte key for PII encryption (ADR-036). Generate: |
|
|
Service-to-service API key for internal endpoints (IEVS, SAVE). Same value across all services. |
|
|
Set |
|
|
Accountable operator override (#1265, audit W2). Outside |
|
|
Accountable operator override (#1412; the #1265 doctrine) for the #1260 DB-TLS guard: outside |
|
|
Accountable operator override (#1412) for the #1260 database-name guard: outside development a |
|
|
Accountable operator override (#1412) for the #1260 object-store guard: outside development the Local ( |
|
|
ADR-042 (#1006) content-scanner backend: |
|
|
Accountable operator override (#1006; the #1265 doctrine): outside |
|
— (required for |
clamd TCP address ( |
|
|
ADR-042 scan-worker tunables, cross-validated at boot (per-exchange deadline < whole-attempt deadline < claim lease; ranges enforced with the env var named in the error). |
|
Required (program services) |
ECDSA P-256 private key PEM for determination signing. Generate: |
|
Required (canopy-eligibility) |
Current public verification key PEM. Retired keys are NOT configured here — they are lazy-loaded from canopy-security’s |
Service-Specific Variables
canopy-applications
| Variable | Default | Description |
|---|---|---|
|
canopy-persons base URL for the finalize cross-service writes |
|
|
|
Finalize-saga op lease (ADR-038): how long a claim fences the operation before a retry may steal it; validated ⇐ 600s at boot |
|
|
Holder-side lease renewal period; validated < the lease at boot |
|
|
Grace beyond lease expiry before the reconciler compensates a stuck operation; validated > the 30s persons request timeout and < 24h |
|
|
Days a terminal finalize-operation row is retained before the reconciler’s pruner may remove it (a completed-but-unreleased op is never pruned) |
|
|
Feature flag: run |
|
(none) |
Server-side key for the finalize request digest (keyed HMAC-SHA256 over the canonical |
canopy-notices
| Variable | Default | Description |
|---|---|---|
|
|
Hours between notice ↔ object-store reconciliation passes (#1215). The pass streams the bucket listing into a Postgres TEMP table in 1 000-key batches and computes orphans/leaks as SQL anti-joins — O(batch) service memory at any caseload. |
|
|
Concurrent work-item claim loops per process (#1217). Safe at any K — claims are |
|
canopy-applications base URL for the lost-credential recovery subscriber (ADR-026) |
|
|
canopy-persons base URL for work-item recipient resolution (#1091) |
|
|
Public applicant-portal base URL for the recovery kill-switch link (applicant-facing, not the compose service name) |
canopy-snap
| Variable | Default | Description |
|---|---|---|
|
canopy-rules service URL for ruleset evaluation |
|
|
canopy-verification service URL for IEVS match queries |
|
|
|
Maximum concurrent detached IEVS verification tasks (#1475). The determine handler try-acquires — never waits — and skips verification with a warning when saturated (verification is advisory; skips are logged). Must be ≥ 1: 0 would silently disable verification, and disablement must be an explicit decision, so it is a boot error. |
|
|
Path to rulesets directory (federal + jurisdiction) |
Token-exchange broker (OIDC A1, #1424)
The two exchanging services (canopy-web, canopy-eligibility) construct the
RFC 8693 TokenExchanger + its chain audit sink at boot when the dedicated
exchanger credentials are set. Fail-closed pairing: setting exactly one of
the pair is a boot error, and canopy-eligibility additionally requires its
SECURITY_URL when the exchanger is configured (the sink must have a chain
to commit into). Absent both, the exchange path stays inert.
| Variable | Default | Description |
|---|---|---|
|
unset (inert) |
canopy-identity client id of the service’s DEDICATED confidential exchanger client ( |
|
unset |
Matching client secret, provider-sourced (SOPS per ADR-017) like |
Receiver contract (OIDC S-slices, #1425+)
The first three are fleet-wide ServiceSettings fields (every service
reads them as CANOPY_<SERVICE>__…), adopted per receiver slice — at HEAD
ALL FIFTEEN receivers set them (the epic &52 chain, #1425–#1439
complete: tanf, medicaid, security, persons, applications,
eligibility, snap, caps, wic, verification, enrollment, renewals,
notices, reporting, appeals) (devstack
values in parentheses); the fourth is the orchestrator’s eligibility-local
SENDER knob for the hop-2 fan-out.
Rollback for a slice = revert the service’s env to the defaults; the
guards degrade to the pre-slice posture with no redeploy of canopy-auth.
| Variable | Default | Description |
|---|---|---|
|
|
Adds the service’s own name (e.g. |
|
unset |
CSV allowlist of exchanger client ids whose exchanged tokens the
receiver contract accepts as |
|
|
Flips |
|
unset |
CSV of program services the orchestrator fans out to WITH a hop-2
exchanged user-context token (requires the eligibility exchanger pair
above). A program absent from the list keeps the legacy service-token
dispatch; unset = no hop-2 exchange anywhere. canopy-chip is deliberately
never listed: CHIP shares the medicaid service, whose single-audience
gate rejects |
|
REQUIRED (yaml: |
canopy-applications base URL — the #596 |
canopy-security (chain-v2 append transport, #1207)
All DORMANT until the #1279 cutover flips CHAIN_V2_APPEND_ENABLED; every
drain/route tunable is domain-validated at startup (out-of-domain = boot
failure, never a silent clamp — plan D8). The staging stats sampler runs
even while dormant.
| Variable | Default | Description |
|---|---|---|
|
|
chain-v2 arm for BOTH audit ingress paths (the |
|
|
Rows claimed per head-lock transaction (domain 1..=500 — the SQL append fn’s batch ceiling; the substrate pinned "revisited by #1207 with throughput evidence"). |
|
|
Drainer pass cadence (domain 10..=60000). |
|
|
Drain transactions per shard per pass (domain 1..=64) — with the rotating start offset, the starvation bound. |
|
|
Unrouted rows claimed per routing transaction (domain 1..=10000). |
|
|
Routing transactions per pass (domain 1..=64) — unbounded routing would starve draining under sustained ingress. |
|
|
|
|
|
|
|
|
Admission cap on staging depth (domain >= 1000): at the sampled cap the consumer nacks (the durable broker remains the overflow home, exactly as today) and ingest returns 503. |
|
|
Backlog-depth degradation threshold for the staging health snapshot (domain >= 1). |
|
|
Oldest-staged-age degradation threshold (domain >= 1). |
Pinned semantics (the #1207 ACs): dwell/flush — a partial batch appends on the next tick; per-shard work is bounded per pass; there is NO shutdown flush (staging is durable — the next boot resumes in O(one claim)). Park policy — classification-driven and single-shot (a deterministic refusal parks the row for the operator unpark runbook), never a numeric retry threshold. Prefetch relationship — the consumer’s prefetch bounds staging INGRESS in-flight per consumer; drain batching is independent; imbalance accumulates in staging where it is measured, with the admission cap bounding the database and pushing true overflow back to the durable broker (where it lives today).
canopy-security (chain-v2 verifiers, #1205 — plan D11)
All DORMANT until the #1279 cutover flips CHAIN_V2_VERIFY_ENABLED; every
tunable is domain-validated at startup by ChainVerifyConfig::from_config
(out-of-domain = startup error, never a silent clamp), and every
relationship below is validated at startup too. Design: plan
chain-v2 verifiers D11.
| Variable | Default | Description |
|---|---|---|
|
|
bool. Master flag for the chain-v2 verifier tasks + verify pools + job servicing. Off (default): no pools, no tasks, no job servicing — |
|
|
Domain 10..=60000. Verifier pass cadence per family task. |
|
|
Domain 0..=3600. Delayed first tick — no boot-time walk. |
|
|
Domain 1..=10000. Rows per bounded verify batch. |
|
|
Domain 4 MiB..=256 MiB — the floor equals the D1a hard row ceiling (4 MiB), so one max-size row ALWAYS fits the budget. |
|
|
Domain 1..=256 — GLOBAL shard VISITS per pass, every visit counted, zero-work refreshes included. |
|
|
Domain 1..=64, validated |
|
|
Domain 30..=86400. Structural-census cadence (family-lease-serialized). |
|
|
Domain 5..=300 — shard AND family leases; also the worst-case crash-recovery pause (expiry-only takeover). |
|
|
Domain 1000..=10000 — |
|
|
Domain 100..=10000. |
|
|
Domain 10..=600, validated |
|
|
Domain 1..=200. Per-batch job heartbeat (claim extension). |
|
|
Domain 1..=10. Claim lapses beyond this finalize the job |
|
|
Domain 1..=64 — passed into |
|
|
Domain ≥30. Read-time tail-freshness bound (any shard over it → |
|
|
Domain ≥1. Read-time tail-lag bound (any shard over it → |
|
|
Domain ≥300 — against |
|
|
Domain ≥300 (#1278 tightens). Trusted-manifest age bound. |
|
— |
String, Debug-redacted (workspace settings pattern). With |
canopy-security (audit archival, #1208)
All serde-defaulted — absent keys = dormant SCHEDULER (deliberately NOT in
default.yaml, the chain-verify-keys precedent). The scheduler flag gates
only the scheduled enqueue path: the archive runner is ALWAYS spawned and
the manual POST /v1/security/archive endpoint is always live regardless of
the flag. Every tunable is domain-validated at boot by
ArchiveConfig::from_config (out-of-domain = startup error, never a silent
clamp), including the two relationship rules (catch-up ≤ interval; lease ≥
3× the chunk timeout) and the required-iff-enabled age threshold. Enablement
procedure: Security Operations › Archive
Management.
| Variable | Default | Description |
|---|---|---|
|
|
bool. Master flag for the SCHEDULED enqueue path only (named for what it does — manual admin enqueues work regardless; the runner is always spawned). Flipping it is the accountable operator override: scheduled runs record |
|
— (no default) |
int, domain 1..=36500. Age threshold in days — rows with |
|
|
int, domain 100..=20000. Rows per mover chunk (each chunk is one atomic per-chunk-committed transaction). |
|
|
int, domain 1..=1000. Chunk budget per runner pass; a pass ending on a full chunk sets |
|
|
int, domain 60..=86400. Scheduler cadence — the due-state row’s claim interval (Skip semantics: a week of downtime = ONE claim). Defaults sustain 5000 × 20 chunks per 300s pass = 28.8M rows/day. |
|
|
int, domain 5..=3600, validated |
|
|
int, domain 1000..=300000. Per-chunk |
|
|
int, domain 10..=600, validated |
|
|
int, domain 1..=10. Claim lapses (lease-expiry reclaims) before a run finalizes |
|
|
int, domain 500..=60000. Runner tick cadence; also feeds the |
|
|
int, domain 0..=3600. Delay before the runner’s first tick — no boot-time pass. |
canopy-tanf
| Variable | Default | Description |
|---|---|---|
|
canopy-rules service URL for ruleset evaluation |
|
|
|
Path to rulesets directory (federal + jurisdiction) |
|
canopy-security URL for signing-key retention registration (ADR-036); unset skips boot registration |
|
|
|
chain-v2 arm for the FTI determination chain entries (#1207, ADR-014 Amendment 7). DORMANT until #1279 — |
canopy-medicaid
| Variable | Default | Description |
|---|---|---|
|
|
Jurisdiction whose parameter table + rulesets load at boot |
|
canopy-rules service URL for ruleset evaluation |
|
|
canopy-persons URL (Express Lane household reads) |
|
|
|
Path to rulesets directory (federal + jurisdiction) |
|
canopy-security URL for signing-key retention registration (ADR-036); unset skips boot registration |
|
|
|
chain-v2 arm for the per-member FTI determination chain entries (#1207, ADR-014 Amendment 7). DORMANT until #1279 — |
canopy-reporting
| Variable | Default | Description |
|---|---|---|
|
canopy-renewals URL for certification queries |
|
|
canopy-persons URL for household queries |
|
|
canopy-applications URL for application queries |
|
|
canopy-enrollment URL for issuance queries |
|
|
canopy-snap URL for ABAWD queries |
|
|
(unset — falls back to |
#1456 (ADR-004 A8b): the dedicated MIGRATION-phase credential for the least-privilege split — bootstrap migrates on this URL over a short-lived pool while the runtime serves as the restricted |
|
|
#1456: the accountable per-control override for the least-privilege boot guard. Outside development an over-broad DB session (privileged attributes, or not |
Docker Compose Variables
These are set in docker-compose.yml for the devstack:
| Variable | Default | Description |
|---|---|---|
|
|
Set on all 19 application services in devstack |
|
|
Rulesets mounted from host into containers |
|
— |
Override URL pointing all program DBs to single PostgreSQL instance |
Port Map
| Service | Port |
|---|---|
canopy-rules |
8001 |
canopy-persons |
8002 |
canopy-applications |
8003 |
canopy-eligibility |
8004 |
canopy-verification |
8005 |
canopy-enrollment |
8006 |
canopy-renewals |
8007 |
canopy-notices |
8008 |
canopy-exchange |
8009 |
canopy-appeals |
8010 |
canopy-reporting |
8011 |
canopy-security |
8012 |
canopy-snap |
8013 |
canopy-web |
8080 |
canopy-portal |
8090 |
Keycloak |
8180 |
PostgreSQL (shared) |
5432 |
PostgreSQL (snap) |
5433 |
PostgreSQL (tanf) |
5434 |
PostgreSQL (medicaid) |
5435 |
PostgreSQL (caps) |
5436 |
PostgreSQL (wic) |
5437 |
RabbitMQ |
5672 (AMQP), 15672 (management) |
Redis |
6379 |
Garage (S3) |
3900 (API), 3903 (web) |