Configuration Reference

On this page

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

CANOPY_{SVC}__PORT

u16

Required

HTTP listen port

CANOPY_{SVC}__DATABASE_URL

String

Required

PostgreSQL connection string. Use ?sslmode=require in production.

CANOPY_{SVC}__RABBITMQ_URL

String

Required

RabbitMQ AMQP URL. Use amqps:// in production.

CANOPY_{SVC}__KEYCLOAK_ISSUER

String

Required

Public Keycloak issuer URL (appears in JWT iss claim)

CANOPY_{SVC}__KEYCLOAK_URL

String

Falls back to KEYCLOAK_ISSUER

Internal URL for JWKS fetching (different from issuer in Docker deployments)

CANOPY_{SVC}__LOG_LEVEL

String

info

Logging level (trace, debug, info, warn, error)

CANOPY_{SVC}__JURISDICTION

String

Required

Jurisdiction identifier for rulesets (e.g., georgia)

CANOPY_{SVC}__CORS_ORIGINS

String

http://localhost:3000,http://localhost:8080

Comma-separated CORS origins or *

CANOPY_{SVC}__BODY_LIMIT

usize

2097152 (2 MiB)

Max request body size in bytes

CANOPY_{SVC}__DB_MAX_CONNECTIONS

u32

10

Database connection pool size

CANOPY_{SVC}__DB_IDLE_TIMEOUT_SECS

u64

600

Idle connection timeout in seconds

CANOPY_{SVC}__RATE_LIMIT_RPM

u64

6000

Rate limit per IP per minute (0 = disabled)

CANOPY_WEB__SESSION_TTL_SECONDS

u64

0 (service default)

Session TTL override for canopy-web (worker BFF) only; default 28800 (8h). canopy-portal does not expose a CANOPY_PORTAL__SESSION_TTL_SECONDS override — per ADR-026 its Redis-primary opaque-token sessions use a TTL derived from the flow_kind of the session (30 min for new applications, 2 h for steady-state, 15 min for kiosk), not a single configurable value.

CANOPY_WEB__CONFIDENTIALITY_FAIL_OPEN

bool

false

#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 discrepancy pill. Setting true restores the pre-#1310 fail-open render for the lookup-FAILURE state only — a household’s own recorded address_confidential/both election is never overridden — and every render that rides the override emits a loud tracing::warn!. The deployment, not the code, owns that risk.

CANOPY_WEB__PAGE_DEADLINE_MS

u64

12000

#1306 aggregate SSR deadline budget for full-page renders (plan ssr-aggregate-deadline). One absolute cutoff is stamped per top-level read handler; every upstream call, retry, and body read derives its residual from it, so a page’s total wall clock is bounded no matter the fan-out. Bounded at boot to [1000, 14000]ms — pages must land under the 15s e2e navigation ceiling — and raising the budget is never the fix for a slow upstream. Out-of-range values refuse to boot unless CANOPY_WEB__DEADLINE_OVERRIDE=true (per-control accountable override, ADR-041 pattern: a loud tracing::warn! every boot names the value and the violated ceiling; the deployment owns the risk; budgets still hard-cap at 600s).

CANOPY_WEB__FRAGMENT_DEADLINE_MS

u64

8000

#1306 aggregate SSR deadline budget for htmx fragments (tab loads, panel retries, /cases/search). Same one-cutoff model and rules as CANOPY_WEBPAGE_DEADLINE_MS, bounded at boot to [1000, 9000]ms — fragments must land under the 10s htmx response wait — with the same CANOPY_WEBDEADLINE_OVERRIDE=true accountable escape hatch.

CANOPY_{SVC}__RATE_LIMIT_REDIS_URL

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 rate-limit-redis feature.

CANOPY_PORTAL__RULESETS_DIR

String

rulesets

Rulesets root the applicant portal reads its jurisdiction config from at boot (#1226). The container image sets /app/rulesets; dx serve from the crate dir needs ../../rulesets.

CANOPY_PORTAL__JURISDICTION

String

georgia

Jurisdiction whose jurisdiction.toml the portal boots against. [notices].agency_phone becomes the helpline rendered on the welcome/help/safety/recover pages, [notices].agency_name the welcome legal line, and [jurisdiction].fips_state_code (via canopy-reference) the state display name in applicant-facing prose (#1273). All fatal at boot if missing — the portal never introduces itself as another state.

CANOPY_NOTICES__WORKER_POLL_MS

u64

2000

#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.

CANOPY_NOTICES__DISPATCHER_POLL_MS

u64

2000

#1367 — the same knob for the notice dispatcher (delivery) loop. Devstack: 200 ms. Same domain and boot posture as WORKER_POLL_MS.

CANOPY_APPEALS__ASSESSMENT_POLL_MS

u64

5000

#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.

CANOPY_ENROLLMENT__SETTLEMENT_TICK_MS

u64

5000

#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.

CANOPY_MQ_DRAINER_MAX_ATTEMPTS

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 /readyz outbox check, and POST /v1/admin/events/replay (#433) unparks it with a fresh budget. Infra failures never count against the budget.

CANOPY_MQ_OUTBOX_PENDING_ALERT_THRESHOLD

i64

10000

Pending (unpublished, unparked) outbox rows above which the /readyz outbox check reports degraded and the drainer WARNs — the ADR-018 alert threshold (#1230). Gauges: canopy_mq_outbox_*.

CANOPY_MQ_OUTBOX_OLDEST_AGE_ALERT_SECS

i64

900

Age bound for the oldest unpublished outbox row before the outbox check degrades (#1230) — catches a small wedged backlog the count threshold never sees.

CANOPY_ENV

String

production

Runtime environment. development relaxes some security checks. Defaults to production (secure by default).

CANOPY_{SVC}__SKIP_MIGRATIONS

bool

false

When true, bootstrap runs NO migrator — migrations are owned by the deploy-time job (cargo xtask migrate apply, #1246 D6; activated per chain service at the #1279 cutover, after which the runtime environment carries no migration-capable credential). In CANOPY_ENV=development it is refused unless __MIGRATIONS_JOB_CONFIGURED is also set, so a dev cannot silently strand a schema.

CANOPY_{SVC}__MIGRATIONS_JOB_CONFIGURED

bool

false

Set ONLY by deployments where the migration job is wired (the devstack chain-migration-split compose profile). Gates __SKIP_MIGRATIONS in development; carries no meaning alone.

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

CANOPY_CHAIN_GENESIS__TARGET_URL

devstack per-service default

The service database cargo xtask chain-genesis installs into (a migration/owner-capable principal — genesis INSERTs into owner-role-owned tables). Passes validate_database_name + the TLS gate.

CANOPY_CHAIN_GENESIS__ANCHOR_URL

devstack canopy_security default

The anchor database for the genesis manifest. The command hard-compares the database name to canopy_security INDEPENDENT of CANOPY_ENV (the ADR-001 guard alone is warn-only in development) — any other database refuses in every environment.

CANOPY_MIGRATE__DATABASE_URL

devstack per-service default

The database cargo xtask migrate apply --service <svc> targets. Same guard posture.

CANOPY_SWEEP_TOKEN

— (required for --apply)

data_steward user bearer for cargo xtask sweep-finalize-orphans --apply (#1055). Env-only by design (never argv). Since #1501 the tool RFC 8693-exchanges it for an exact aud=canopy-persons user-context token before the first POST (the compensate route is user-only under the #1428 receiver contract), so one password-grant mint suffices — see the runbook.

CANOPY_SWEEP_EXCHANGER_CLIENT_ID / CANOPY_SWEEP_EXCHANGER_SECRET

canopy-web-exchanger / its devstack secret

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.

CANOPY_SWEEP_KEYCLOAK_URL / CANOPY_SWEEP_KEYCLOAK_REALM

.ports.env devstack port / canopy

Keycloak issuer for the #1501 sweep-tool exchange (--keycloak-url overrides the URL).

CANOPY_CLI_EXCHANGER_CLIENT_ID / CANOPY_CLI_EXCHANGER_SECRET

canopy-web-exchanger / its devstack secret

Exchanger client for canopy-cli’s crypto-shred commands (`canopy person redact-ssn, canopy income/asset/expense redact, #1501): the stored login bearer is exchanged for aud=canopy-persons before the POST (same user-only contract as above). Same default-vs-production posture as the sweep pair.

Security Variables

Variable Default Description

CANOPY_ENCRYPTION_KEY

Required

AES-256-GCM base64-encoded 32-byte key for PII encryption (ADR-036). Generate: openssl rand -base64 32. Required by canopy-persons, the five program services, and — since #1256 — canopy-reporting (T-MSIS extract sealing, ADR-004 A8a); those services refuse to boot without it. canopy-reporting checks it before bootstrap-owned migrations run, so the sealing reshape cannot commit and strand an unsealable service. (In job-owned-migration deployments — SKIP_MIGRATIONS — the deploy job applies migrations separately; the service still refuses boot without the key, but the deploy job should set it too so the ordering guarantee holds there.)

CANOPY_INTERNAL_API_KEY

canopy-internal-dev-key

Service-to-service API key for internal endpoints (IEVS, SAVE). Same value across all services.

CANOPY_SESSION_SECURE

true

Set Secure flag on session cookies. Set to false only in development (HTTP).

CANOPY_VERIFICATION__ALLOW_FABRICATED_VERIFICATION

false

Accountable operator override (#1265, audit W2). Outside CANOPY_ENV=development a noop-adapters build of canopy-verification refuses to boot — it would serve fabricated IEVS/SAVE/SSA data. true boots anyway with a loud, auditable startup warning; the deployment owns the risk (fail-closed default + explicit override, the ADR-041 doctrine).

CANOPY_DB__ALLOW_UNENCRYPTED_CONNECTION

false

Accountable operator override (#1412; the #1265 doctrine) for the #1260 DB-TLS guard: outside CANOPY_ENV=development a DATABASE_URL without sslmode=require/verify-ca/verify-full refuses to boot. true (exactly, lowercase — all three #1412 flags read the environment directly with this strictness; 1/on/TRUE are ignored) boots anyway with a loud per-boot warning; database traffic, including PII, is then plaintext in transit. Full inventory: Startup-Guard Inventory.

CANOPY_DB__ALLOW_NAME_MISMATCH

false

Accountable operator override (#1412) for the #1260 database-name guard: outside development a DATABASE_URL whose database name does not match the service (ADR-001 program isolation) refuses to boot. true boots anyway with a loud per-boot warning — cross-service isolation then rests on the deployment’s own naming discipline.

CANOPY_STORE__ALLOW_LOCAL_BACKEND

false

Accountable operator override (#1412) for the #1260 object-store guard: outside development the Local (/tmp) backend refuses to boot (objects lost on restart, not shared across replicas). true boots anyway with a loud per-boot warning.

CANOPY_APPLICATIONS__SCANNER_BACKEND

clamav

ADR-042 (#1006) content-scanner backend: clamav (clamd INSTREAM sidecar) or noop. Selecting noop outside development trips the fail-closed guard below.

CANOPY_APPLICATIONS__ALLOW_INSECURE_SCANNER

false

Accountable operator override (#1006; the #1265 doctrine): outside CANOPY_ENV=development, scanner_backend=noop refuses to boot — every citizen upload would settle clean without inspection. true boots anyway with a loud per-boot warning; the deployment owns the risk.

CANOPY_APPLICATIONS__CLAMD_ADDR

— (required for clamav)

clamd TCP address (host:port; devstack: clamav:3310). Unauthenticated protocol — never expose beyond the service network (runbook).

CANOPY_APPLICATIONS__CLAMD_TIMEOUT_SECS / SCAN_ATTEMPT_TIMEOUT_SECS / SCAN_POLL_SECS / SCAN_WORKER_CONCURRENCY / SCAN_MAX_ATTEMPTS / SCAN_LEASE_SECS / SCANNER_MAX_DEFINITION_AGE_DAYS

30 / 120 / 5 / 2 / 8 / 600 / 7

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). SCAN_WORKER_CONCURRENCY=0 is the documented kill switch (ERROR log + the canopy_applications_scan_worker_disabled gauge; uploads strand pending). Attempts count CLAIMS: the claim that brings a row to SCAN_MAX_ATTEMPTS settles terminal error — at most N−1 real scans. Definitions older than the max age fail scanning closed (uploads stay pending; serving is unaffected).

CANOPY_{PROGRAM}__SIGNING_KEY

Required (program services)

ECDSA P-256 private key PEM for determination signing. Generate: cargo xtask gen-signing-keys --program snap

CANOPY_VERIFY_KEY_{PROGRAM}

Required (canopy-eligibility)

Current public verification key PEM. Retired keys are NOT configured here — they are lazy-loaded from canopy-security’s signing_key_history (T2-6 / ADR-036); the old _PREV dual-key slot was removed.

Service-Specific Variables

canopy-applications

Variable Default Description

CANOPY_APPLICATIONS__PERSONS_URL

http://canopy-persons:8002

canopy-persons base URL for the finalize cross-service writes

CANOPY_APPLICATIONS__FINALIZE_LEASE_SECS

30

Finalize-saga op lease (ADR-038): how long a claim fences the operation before a retry may steal it; validated ⇐ 600s at boot

CANOPY_APPLICATIONS__FINALIZE_HEARTBEAT_SECS

10

Holder-side lease renewal period; validated < the lease at boot

CANOPY_APPLICATIONS__FINALIZE_RECONCILER_GRACE_SECS

3600

Grace beyond lease expiry before the reconciler compensates a stuck operation; validated > the 30s persons request timeout and < 24h

CANOPY_APPLICATIONS__FINALIZE_COMPLETED_RETENTION_DAYS

30

Days a terminal finalize-operation row is retained before the reconciler’s pruner may remove it (a completed-but-unreleased op is never pruned)

CANOPY_APPLICATIONS__FINALIZE_SAGA_ENABLED

true

Feature flag: run finalize_draft as the ADR-038 saga. Default-ON since epic &71 MR8 (the canopy-persons receipt surface — MR1/MR2 — ships in the same tree, satisfying the deploy-order contract). While on, boot fails fast unless the digest secret below is configured; set false only to fall back to the legacy non-idempotent path

CANOPY_APPLICATIONS__FINALIZE_DIGEST_SECRET

(none)

Server-side key for the finalize request digest (keyed HMAC-SHA256 over the canonical FinalizeRequest, so the digest pinned in finalize_operations is not offline-guessable from applicant PII). Supply via env; the shipped config YAML carries no value and the secrets-yaml-lint CI gate rejects plaintext secrets in config/*/.yaml. Required >= 32 bytes at boot when the saga flag is on; Debug output is redacted

canopy-notices

Variable Default Description

CANOPY_NOTICES__RECONCILE_INTERVAL_HOURS

24

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. 0 disables the loop entirely (the operator kill switch / off-peak scheduling lever)

CANOPY_NOTICES__WORKER_CONCURRENCY

4

Concurrent work-item claim loops per process (#1217). Safe at any K — claims are FOR UPDATE SKIP LOCKED-disjoint and completes lease-fenced — so this is purely a throughput lever; Typst renders still serialize behind the single render thread until #1192. 0 disables the worker loops (items queue durably, drain on restart). Scale-out signals: the canopy_notices_work_item_{queue_depth,oldest_age_seconds,terminal_failed} gauges (30s poll, OTLP push)

CANOPY_NOTICES__APPLICATIONS_URL

http://canopy-applications:8003

canopy-applications base URL for the lost-credential recovery subscriber (ADR-026)

CANOPY_NOTICES__PERSONS_URL

http://canopy-persons:8002

canopy-persons base URL for work-item recipient resolution (#1091)

CANOPY_NOTICES__PORTAL_BASE_URL

http://localhost:8080

Public applicant-portal base URL for the recovery kill-switch link (applicant-facing, not the compose service name)

canopy-snap

Variable Default Description

CANOPY_SNAP__RULES_URL

http://localhost:8001

canopy-rules service URL for ruleset evaluation

CANOPY_SNAP__VERIFICATION_URL

http://localhost:8005

canopy-verification service URL for IEVS match queries

CANOPY_SNAP__IEVS_CONCURRENCY

4

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.

CANOPY_RULESETS_DIR

rulesets

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

CANOPY_WEBOIDC_EXCHANGER_CLIENT_ID / CANOPY_ELIGIBILITYOIDC_EXCHANGER_CLIENT_ID

unset (inert)

canopy-identity client id of the service’s DEDICATED confidential exchanger client (canopy-web-exchanger / canopy-eligibility-exchanger in the devstack realm). Distinct from the ADR-019 service client by design — no service:* role, mints only ≤300s user-context tokens.

CANOPY_WEBOIDC_EXCHANGER_CLIENT_SECRET / CANOPY_ELIGIBILITYOIDC_EXCHANGER_CLIENT_SECRET

unset

Matching client secret, provider-sourced (SOPS per ADR-017) like OIDC_SERVICE_CLIENT_SECRET so secret-access audit logging fires.

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

CANOPY_<SVC>__ACCEPT_OWN_AUDIENCE

false

Adds the service’s own name (e.g. canopy-tanf) to the JWT audiences its bootstrap accepts — the prerequisite for exchanged per-target tokens to pass validation at all. (all adopters: true)

CANOPY_<SVC>__AUTHORIZED_EXCHANGER_AZPS

unset

CSV allowlist of exchanger client ids whose exchanged tokens the receiver contract accepts as azp. Unset/empty fails closed: every exchanged-shaped bearer is 403 azp_not_allowlisted. (tanf, medicaid, snap, caps, wic, security: canopy-web-exchanger,canopy-eligibility-exchanger; persons, applications, eligibility, verification, enrollment, renewals, notices, reporting, appeals: canopy-web-exchanger ONLY — least privilege: none is a target another exchanger legitimately mints for — eligibility’s own exchanger mints for the fan-out TARGETS, never for eligibility itself. Never the devstack-only conformance exchanger)

CANOPY_<SVC>__ENFORCE_USER_ONLY_ROUTES

false

Flips require_user_only routes (tanf: FTI audit log, redaction; medicaid: those plus the ELE revoke + renewals-run ops routes; security: the archive POST + bulk audit export; persons: the redact pair, compensate-finalize-orphan, and the bulk export; applications: NONE — zero user-only routes exist, the flag is inert and set for fleet consistency — verification, enrollment, renewals, notices, and appeals likewise have zero user-only routes; eligibility: the six bulk-run mutations — create/enact/pause/resume/cancel/retry-failures; snap: the determination redact + the QC export; caps + wic: the determination redact; reporting: ALL 21 supervisor report surfaces — the largest user-only set in the fleet, #1438) from transitional — legacy broad-audience worker bearers still pass the role bar — to enforced: only an exchanged per-target token carrying the role passes; service class is 403 on these routes regardless of this flag. (all adopters: true)

CANOPY_ELIGIBILITY__EXCHANGE_TARGETS

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 aud=canopy-chip. (devstack: canopy-tanf,canopy-medicaid,canopy-snap,canopy-caps,canopy-wic)

CANOPY_ELIGIBILITY__APPLICATIONS_URL

REQUIRED (yaml: applications_url)

canopy-applications base URL — the #596 household_assignments lookup behind the scoped cross-program-alerts feed (PUB-1075 AC-6). REQUIRED: boot fails without it, and the value is semantically validated at startup (http/https only, no query or fragment, trailing slash normalized) — a malformed value must fail the boot, not surface as a fail-closed 502 on every scoped read. The scoped feed has NO unscoped fallback and no deployment override (ratified #596 spec). Deployment constraint: workers must authenticate through a single UUID-subject issuer (the assignment substrate keys on UUID-projected subjects; the canonical issuer+subject redesign is #1008). (devstack: http://canopy-applications:8003)

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

CANOPY_SECURITY__CHAIN_V2_APPEND_ENABLED

false

chain-v2 arm for BOTH audit ingress paths (the #-queue consumer and POST /v1/security/audit/ingest). false = v1 advisory-lock chain, byte-identical; true = durable staging + the per-shard drainer (ADR-014 Amendment 7).

CANOPY_SECURITY__CHAIN_DRAIN_BATCH_SIZE

500

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").

CANOPY_SECURITY__CHAIN_DRAIN_TICK_MS

250

Drainer pass cadence (domain 10..=60000).

CANOPY_SECURITY__CHAIN_DRAIN_MAX_BATCHES_PER_SHARD

4

Drain transactions per shard per pass (domain 1..=64) — with the rotating start offset, the starvation bound.

CANOPY_SECURITY__CHAIN_ROUTE_BATCH_SIZE

1024

Unrouted rows claimed per routing transaction (domain 1..=10000).

CANOPY_SECURITY__CHAIN_ROUTE_MAX_BATCHES

4

Routing transactions per pass (domain 1..=64) — unbounded routing would starve draining under sustained ingress.

CANOPY_SECURITY__CHAIN_DRAIN_LOCK_TIMEOUT_MS

5000

SET LOCAL lock_timeout on every route/drain transaction (domain 100..=60000) — one leg of the bounded structural-lease release (Amendment 7).

CANOPY_SECURITY__CHAIN_DRAIN_STATEMENT_TIMEOUT_MS

30000

SET LOCAL statement_timeout on every route/drain transaction (domain 1000..=300000) — the other release leg.

CANOPY_SECURITY__CHAIN_STAGING_MAX_DEPTH

500000

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.

CANOPY_SECURITY__CHAIN_STAGING_ALERT_DEPTH

10000

Backlog-depth degradation threshold for the staging health snapshot (domain >= 1).

CANOPY_SECURITY__CHAIN_STAGING_ALERT_AGE_SECS

300

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

CANOPY_SECURITY__CHAIN_V2_VERIFY_ENABLED

false

bool. Master flag for the chain-v2 verifier tasks + verify pools + job servicing. Off (default): no pools, no tasks, no job servicing — GET /v1/security/chain/status serves unknown → 503 and POST /v1/security/chain/verify returns 503 verifier_unavailable (no phantom queue).

CANOPY_SECURITY__CHAIN_VERIFY_TICK_MS

1000

Domain 10..=60000. Verifier pass cadence per family task.

CANOPY_SECURITY__CHAIN_VERIFY_FIRST_TICK_DELAY_SECS

60

Domain 0..=3600. Delayed first tick — no boot-time walk.

CANOPY_SECURITY__CHAIN_VERIFY_BATCH_SIZE

1000

Domain 1..=10000. Rows per bounded verify batch.

CANOPY_SECURITY__CHAIN_VERIFY_BATCH_BYTES

33554432 (32 MiB)

Domain 4 MiB..=256 MiB — the floor equals the D1a hard row ceiling (4 MiB), so one max-size row ALWAYS fits the budget.

CANOPY_SECURITY__CHAIN_VERIFY_PASS_BUDGET

16

Domain 1..=256 — GLOBAL shard VISITS per pass, every visit counted, zero-work refreshes included.

CANOPY_SECURITY__CHAIN_SCRUB_BATCHES_PER_PASS

1

Domain 1..=64, validated < CHAIN_VERIFY_PASS_BUDGET (the scrub share can never starve the tail).

CANOPY_SECURITY__CHAIN_CENSUS_INTERVAL_SECS

300

Domain 30..=86400. Structural-census cadence (family-lease-serialized).

CANOPY_SECURITY__CHAIN_VERIFY_LEASE_SECS

30

Domain 5..=300 — shard AND family leases; also the worst-case crash-recovery pause (expiry-only takeover).

CANOPY_SECURITY__CHAIN_VERIFY_STATEMENT_TIMEOUT_MS

5000

Domain 1000..=10000 — from_config validates lease_secs × 1000 ≥ 3 × statement_timeout_ms as a LIVENESS heuristic (correctness rides the token; the check keeps a healthy holder from being contested mid-batch, nothing more).

CANOPY_SECURITY__CHAIN_VERIFY_LOCK_TIMEOUT_MS

2000

Domain 100..=10000. SET LOCAL lock_timeout on every verifier security-pool statement.

CANOPY_SECURITY__CHAIN_JOB_CLAIM_SECS

60

Domain 10..=600, validated ≥ 3 × CHAIN_JOB_HEARTBEAT_SECS. Manual verify-job claim lease.

CANOPY_SECURITY__CHAIN_JOB_HEARTBEAT_SECS

15

Domain 1..=200. Per-batch job heartbeat (claim extension).

CANOPY_SECURITY__CHAIN_JOB_MAX_ATTEMPTS

3

Domain 1..=10. Claim lapses beyond this finalize the job error/crashed.

CANOPY_SECURITY__CHAIN_JOB_MAX_QUEUED

8

Domain 1..=64 — passed into chain_job_enqueue (the DB fns read no config); at the cap the verify endpoint returns 503 verifier_unavailable.

CANOPY_SECURITY__CHAIN_TAIL_MAX_AGE_SECS

300

Domain ≥30. Read-time tail-freshness bound (any shard over it → stale).

CANOPY_SECURITY__CHAIN_TAIL_MAX_LAG

100000

Domain ≥1. Read-time tail-lag bound (any shard over it → stale).

CANOPY_SECURITY__CHAIN_SCRUB_MAX_AGE_SECS

172800

Domain ≥300 — against cycle_completed_at, falling back to cycle_started_at on a never-completed first cycle.

CANOPY_SECURITY__CHAIN_MANIFEST_MAX_AGE_SECS

604800

Domain ≥300 (#1278 tightens). Trusted-manifest age bound.

CANOPY_SECURITY__CHAIN_VERIFY_DATABASE_URL / …_CHAIN_VERIFY_TANF_DATABASE_URL / …_CHAIN_VERIFY_MEDICAID_DATABASE_URL

String, Debug-redacted (workspace settings pattern). With CHAIN_V2_VERIFY_ENABLED, each set URL spawns that family’s verifier task (audit; fti/canopy-tanf and fti/canopy-medicaid since #1206 MR-3) — the audit URL is required when enabled, the TANF/Medicaid URLs are optional and their family stays DORMANT when absent (typed 503 unknown/verifier_disabled, per-family, X10). Validation is parse-only at boot; CONNECTIVITY is lazy per family — one unreachable program DB degrades only ITS family at read time, never the process. The login carrier (canopy_security_verify) is NOLOGIN until #1279.

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

CANOPY_SECURITY__ARCHIVE_SCHEDULER_ENABLED

false

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 requested_by = 'scheduler' with their frozen config snapshot on the run row.

CANOPY_SECURITY__ARCHIVE_AFTER_DAYS

— (no default)

int, domain 1..=36500. Age threshold in days — rows with received_at older than this move to audit_events_archive. Required iff the scheduler is enabled (boot error names the key when absent); deliberately no default — the threshold is an operator decision and no retention policy is embedded (retention = archive ∪ live; policy = #1303). NOT a retention value: the archive retains indefinitely.

CANOPY_SECURITY__ARCHIVE_CHUNK_SIZE

5000

int, domain 100..=20000. Rows per mover chunk (each chunk is one atomic per-chunk-committed transaction).

CANOPY_SECURITY__ARCHIVE_MAX_CHUNKS_PER_PASS

20

int, domain 1..=1000. Chunk budget per runner pass; a pass ending on a full chunk sets more = true on the run.

CANOPY_SECURITY__ARCHIVE_INTERVAL_SECS

300

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.

CANOPY_SECURITY__ARCHIVE_CATCHUP_INTERVAL_SECS

30

int, domain 5..=3600, validated ARCHIVE_INTERVAL_SECS. The more = true catch-up cadence — a full-chunk pass pulls the next due time forward to this, so backlogs drain immediately and boundedly.

CANOPY_SECURITY__ARCHIVE_STATEMENT_TIMEOUT_MS

30000

int, domain 1000..=300000. Per-chunk SET LOCAL statement_timeout; a timed-out chunk rolls back whole (error/statement_timeout, committed progress stands).

CANOPY_SECURITY__ARCHIVE_LEASE_SECS

120

int, domain 10..=600, validated >= 3×(ARCHIVE_STATEMENT_TIMEOUT_MS/1000) — a LIVENESS heuristic (correctness rides the fencing token; the check keeps a healthy holder from being contested mid-chunk). Run-lease duration; expiry-only takeover.

CANOPY_SECURITY__ARCHIVE_MAX_ATTEMPTS

3

int, domain 1..=10. Claim lapses (lease-expiry reclaims) before a run finalizes error/crashed — committed progress intact.

CANOPY_SECURITY__ARCHIVE_RUNNER_TICK_MS

5000

int, domain 500..=60000. Runner tick cadence; also feeds the /readyz audit-archive staleness window (max(3×tick, 60s), non-gating).

CANOPY_SECURITY__ARCHIVE_FIRST_TICK_DELAY_SECS

60

int, domain 0..=3600. Delay before the runner’s first tick — no boot-time pass.

canopy-tanf

Variable Default Description

CANOPY_TANF__RULES_URL

http://canopy-rules:8001

canopy-rules service URL for ruleset evaluation

CANOPY_TANF__RULESETS_DIR

rulesets

Path to rulesets directory (federal + jurisdiction)

CANOPY_TANF__SECURITY_URL

http://canopy-security:8012

canopy-security URL for signing-key retention registration (ADR-036); unset skips boot registration

CANOPY_TANF__CHAIN_V2_APPEND_ENABLED

false

chain-v2 arm for the FTI determination chain entries (#1207, ADR-014 Amendment 7). DORMANT until #1279false keeps the v1 fti_audit_log advisory-lock path byte-identical; true appends to the sharded fti_audit_log_v2 substrate FAIL-CLOSED (an unappendable chain aborts the determination with 503). The cutover runbook flips it only after epoch activation.

canopy-medicaid

Variable Default Description

CANOPY_MEDICAID__JURISDICTION

georgia

Jurisdiction whose parameter table + rulesets load at boot

CANOPY_MEDICAID__RULES_URL

http://canopy-rules:8001

canopy-rules service URL for ruleset evaluation

CANOPY_MEDICAID__PERSONS_URL

http://canopy-persons:8002

canopy-persons URL (Express Lane household reads)

CANOPY_MEDICAID__RULESETS_DIR

rulesets

Path to rulesets directory (federal + jurisdiction)

CANOPY_MEDICAID__SECURITY_URL

http://canopy-security:8012

canopy-security URL for signing-key retention registration (ADR-036); unset skips boot registration

CANOPY_MEDICAID__CHAIN_V2_APPEND_ENABLED

false

chain-v2 arm for the per-member FTI determination chain entries (#1207, ADR-014 Amendment 7). DORMANT until #1279false keeps the v1 fti_audit_log advisory-lock path byte-identical; true appends to the sharded fti_audit_log_v2 substrate FAIL-CLOSED (an unappendable chain aborts the determination with 503). The cutover runbook flips it only after epoch activation.

canopy-reporting

Variable Default Description

CANOPY_REPORTING__RENEWALS_URL

http://localhost:8007

canopy-renewals URL for certification queries

CANOPY_REPORTING__PERSONS_URL

http://localhost:8002

canopy-persons URL for household queries

CANOPY_REPORTING__APPLICATIONS_URL

http://localhost:8003

canopy-applications URL for application queries

CANOPY_REPORTING__ENROLLMENT_URL

http://localhost:8006

canopy-enrollment URL for issuance queries

CANOPY_REPORTING__SNAP_URL

http://localhost:8013

canopy-snap URL for ABAWD queries

CANOPY_REPORTING__MIGRATION_DATABASE_URL

(unset — falls back to DATABASE_URL)

#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 canopy_reporting_app login on DATABASE_URL. The field exists fleet-wide (CANOPY_{SVC}__MIGRATION_DATABASE_URL); reporting is the first consumer.

CANOPY_REPORTING__ALLOW_BROAD_DB_ROLE

false

#1456: the accountable per-control override for the least-privilege boot guard. Outside development an over-broad DB session (privileged attributes, or not canopy_reporting_app) refuses to start; true proceeds with a loud auditable WARN naming the cutover runbook.

canopy-web (Worker Portal BFF)

Variable Default Description

All 8 service client URLs

http://localhost:{port}

One URL per backend service (persons, applications, eligibility, snap, renewals, notices, appeals, security). Configured in clients.rs.

Docker Compose Variables

These are set in docker-compose.yml for the devstack:

Variable Default Description

CANOPY_ENV

development

Set on all 19 application services in devstack

CANOPY_RULESETS_DIR

/app/rulesets

Rulesets mounted from host into containers

CANOPY_SNAP_DB_URL (shared-db mode)

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)

Edit this page · default