Canopy Service Catalog

On this page

This page is the canonical, human- and agent-facing catalog of Canopy’s services. It answers "what services exist, what do they own, and where is the detail?" Endpoint-by-endpoint request/response detail lives in the per-service API Reference pages; schema detail lives in the per-service Data Model pages. This catalog ties them together and carries the cross-service overview that has no other home.

Canopy follows ADR-001 program-service isolation: each benefit program is an independent service with its own PostgreSQL database, and program services never read each other’s databases. Cross-service communication is HTTP returning signed determination objects only (ADR-002).

Service topology

NOTE

The port numbers below are each service’s container-internal listen port — stable and fixed per service. The host-published port is not these numbers: cargo xtask dev start reserves a free OS-ephemeral host port per service (it binds 127.0.0.1:0 and lets the OS pick), exports it as CANOPY_PORT_<SERVICE>_<CONTAINER>, and docker-compose.yml interpolates ${CANOPY_PORT_…:-<default>}. This lets several Canopy stacks run on one host without port collisions, so host ports are not deterministic across dev start invocations.

Discover the live host mapping with cargo xtask dev status (it prints a URL table) or read .ports.env (gitignored). The integration and E2E harnesses discover ports the same way — never hardcode a host port. The defaults below double as the fallback host port only when the CANOPY_PORT_* var is unset (e.g. plain docker compose up, which is not the supported path — always use cargo xtask dev).

See Configuration Reference for the full port/env map and ADR-005 for deployment profiles.

Infrastructure services

These run against the shared postgres:5432 instance (one logical database each) and carry no legally-restricted data tenancy of their own.

Service Port Database Role

canopy-rules

8001

canopy_rules

zen-engine JDM evaluation, shared by every program service (ADR-003).

canopy-persons

8002

canopy_persons

Person, household, income, asset, expense, and address management.

canopy-applications

8003

canopy_applications

ACA §1413 single-streamlined intake, expedited screening, intake sections, authorized representatives, applicant-portal credential verification + client-side-encrypted Apply-form drafts (ADR-026).

canopy-eligibility

8004

canopy_eligibility

Orchestrator — fans out to program services, verifies JWS determinations, applies the cross-program hierarchy.

canopy-verification

8005

canopy_verification

Federal hub adapters (IEVS, SAVE, SSA SDX/BENDEX); verification + IEVS-hit work items.

canopy-enrollment

8006

canopy_enrollment

Post-determination enrollment and SNAP EBT benefit issuance.

canopy-renewals

8007

canopy_renewals

Certification periods, interim contacts, change reports, renewal scheduler.

canopy-notices

8008

canopy_notices

Typst-rendered notices/forms; PDF storage in S3.

canopy-exchange

8009

canopy_exchange

FFE account transfer (Georgia Access). Stub — trait defined, no methods (partner-blocked).

canopy-appeals

8010

canopy_appeals

Fair hearings + IPV/ADH disqualification workflow.

canopy-reporting

8011

canopy_reporting

Federal reporting extracts (FNS-388/7176, ACF-199/196, T-MSIS, CMS-64/416) + cross-program overpayment roll-up.

canopy-security

8012

canopy_security

Audit-event sink, NIST control mappings, breach detection, hash-chain verification, archival, scoped fact change-history (epic &56 / T1-6).

Program services (ADR-001 isolated databases)

Each program service owns a dedicated PostgreSQL instance so legally-restricted data is isolated with independent audit (ADR-004).

Service Port Database Restricted data tenancy

canopy-snap

8013

canopy_snap (postgres-snap)

IEVS, SSA SOLQ/BINDEX (SNAP CMA).

canopy-tanf

8014

canopy_tanf (postgres-tanf)

FTI (IRC §6103(l)(7)), SSA SOLQ/BINDEX (TANF CMA). FTI audit hash chain per ADR-014.

canopy-medicaid

8015

canopy_medicaid (postgres-medicaid)

FTI (IRC §6103(l)(12)), FDSH, HIPAA-scoped. FTI audit hash chain per ADR-014.

canopy-caps

8016

canopy_caps (postgres-caps)

None (state-administered CCDF).

canopy-wic

8017

canopy_wic (postgres-wic)

None (state-administered).

Backend-for-frontend (BFF) services

Service Port Role

canopy-web

8080

Worker portal — Axum + Askama + htmx + Alpine.js (CSP build). Three composition-driven dashboard surfaces (worker / supervisor / analyst) dispatched per WorkerRole; case search, composition-driven case detail, caseworker action handlers — including the per-member fact editors for income / assets / expenses / address (#983, effective-dated /claims authoring), "Customize my dashboard."

canopy-portal

8090

Applicant portal — Dioxus 0.7 fullstack (privacy-first BFF; ADR-008 + ADR-026). Fluent i18n wired; Redis-primary sessions, Postgres-free; domain routes shipped with the Dioxus app (Plan 3 complete).

Session and database configuration

Each BFF has its own database on the shared PostgreSQL instance for session storage via tower-sessions-sqlx-store::PostgresStore (ADR-009MemoryStore is banned). Exception: canopy-portal is Postgres-free and uses Redis-primary sessions per ADR-026.

Service Database Session policy

canopy-web

canopy_web

8-hour sliding TTL; HttpOnly; SameSite=Strict (double-redirect via /auth/landing for the Keycloak OIDC callback); Secure configurable via CANOPY_SESSION_SECURE.

canopy-portal

n/a — Redis-primary (ADR-026); Postgres deprovision pending

Redis-primary opaque session tokens (ADR-026), flow_kind-derived TTL (30 min apply/recovery/renewal · 2 h steady-state · 15 min kiosk); HttpOnly; SameSite=Strict; Secure configurable. Fluent i18n live (en + es bundles, LocaleManager + LocaleExt extractor — plumbing-only until the first domain route).

Cross-cutting concerns

Transactional outbox (event_outbox, ADR-018)

Every service carries an event_outbox table per ADR-018 — it is provisioned uniformly (#471) so the in-process OutboxDrainer spawned at canopy-api bootstrap always has a relation to drain. The services that actually publish domain events are all infrastructure and program services plus canopy-rules (rules.evaluated) and canopy-web (composition audit events per ADR-022); canopy-exchange (stub) carries the table but currently publishes nothing; canopy-portal is a Postgres-free Dioxus BFF (ADR-026) with no database, so it carries no event_outbox and publishes nothing. The publisher writes the outbox row inside the caller’s transaction; the OutboxDrainer flushes unpublished rows to the RabbitMQ topic exchange canopy.events, and an in-process janitor sweeps published rows older than seven days. The schema is identical across services:

(id UUID, routing_key TEXT, payload JSONB, enqueued_at TIMESTAMPTZ,
 published_at TIMESTAMPTZ NULL, attempts INT, last_error TEXT,
 claimed_at TIMESTAMPTZ NULL, claimed_by TEXT NULL)

with a partial index on (enqueued_at) WHERE published_at IS NULL and a lease index on (claimed_at NULLS FIRST, enqueued_at) WHERE published_at IS NULL — the claimed_at/claimed_by lease columns coordinate multi-replica draining. Event payloads carry IDs, status codes, timestamps, and program codes — plus, for the T1-5 (#673) attributed fact events, typed author + fact values (ADR-027 §4) — but never FTI, SSA, IEVS, or HIPAA-scoped fields (the canopy-mq publisher’s restricted-field guard is the backstop).

Signed determinations (ADR-002)

Program services never return raw eligibility data. Each returns a JWS-signed determination object (ECDSA P-256) that the orchestrator verifies against a VerifyingKeyRegistry per ADR-002. Determinations are append-only: once signed and accepted they are superseded by a new determination, never modified in place.

Audit and FTI hash chains (ADR-014)

canopy-security’s wildcard (#) subscriber captures every event on canopy.events and persists a JWS-signed audit_events row with a SHA-256 previous_hash/event_hash chain. canopy-tanf and canopy-medicaid additionally maintain a separate fti_audit_log hash chain for IRS Publication 1075 audit. Audit/FTI chain verification is the chain-v2 checkpointed verifier (#1205, ADR-014 Amendment 9): the unified /v1/security/chain/* namespace (status / manual verify jobs / per-event attestation), family-leased background tail+scrub verifiers in canopy-security — DORMANT until the #1279 cutover, so status reports 503 unknown (fail closed; a latched breach stays visible as breached). The FTI arm is ACTIVATED (#1206 MR-3): per-family verifier tasks for canopy-tanf/canopy-medicaid spawn when their verify-pool URLs are configured, family=fti&service=… serves live status/attest/verify, a latched legacy v1 breach forces breached/legacy_breach_latched even while dormant, and the legacy fti/chain-status interim surface is DELETED. The medicaid ELE chain’s own status walk migrates with #1248. The chain-v2 substrate itself (#1246, ADR-014 Amendment 6) is fully landed and DORMANT in all three chain databases — protocol crate (canopy-chain), schema/roles/append functions, and the cargo xtask chain-genesis installer (runbook); the v1→v2 cutover is #1279.

That same ledger backs the scoped fact change-history endpoint (GET /v1/security/persons/{id}/fact-history/{resource}, epic &56 / T1-6) — the transaction-time history of a person’s eligibility-fact claims/corrections/closes (ADR-027 §4), composed into a household view at the canopy-web BFF. See the canopy-security API reference.

Per-service reference

Each entry summarizes the service’s role and the events it publishes/subscribes. Endpoint tables and request/response schemas live in the linked API page; table inventories and ER diagrams live in the linked Data Model page.

canopy-rules

JDM ruleset evaluation engine wrapping zen_engine::DecisionEngine. Rulesets are filesystem-backed (ADR-003) — rulesets/federal/ and rulesets/{jurisdiction}/ are scanned at startup by a NamedFilesystemLoader; there is no runtime mutation path. Evaluations run on a multi-worker LocalPoolHandle (2–16, CANOPY_RULES__EVAL_WORKERS) because the evaluate future is !Send. Publishes rules.evaluated.

API

canopy-rules API

Data model

canopy-rules schema (rule_evaluations audit trail)

canopy-persons

CRUD for persons, households, and addresses. Facts (income, assets, expenses) live only in the valid-time *_versions corpus (T1-4 Slice 3 / #672 / epic &56): authored writes via POST …/{income,assets,expenses}/claims (retroactive-correction algorithm, ADR-027) + income close via DELETE …/income/claims/{fact_id}; every fact read (per-person GET, /households/{id}/full?as_of, :batchGet) is an as-of valid-time read of the corpus, claim_status-filtered to determination-feeding and carrying provenance. The legacy fact tables + write endpoints + backfill are dropped. Publishes person.created, person.updated, household.created, household.member_added, household.member_removed, and the T1-5 (#673) attributed fact events income.claimed / asset.claimed / expense.claimed / income.closed — these carry typed author + fact values (ADR-027 §4) in the payload, never raw identity or FTI/IEVS (ADR-027 §8 / ADR-004); canopy-security indexes them by fact_id + author.sub. Also publishes the figure-free persons.income_changed signal (#652) on income claim/close — person_id + household_id only, never a figure (ADR-004) — so canopy-medicaid can re-evaluate Express Lane income lapse in real time. Carries the ADR-038 finalize surface: receipt-tagged idempotent create/claim writes + the applications-only /v1/internal/finalize-operations/{op}/{gen}/{register,release,cancel} control endpoints (epic &71 MR1/MR2), and the data_steward-gated POST /v1/households/{id}/compensate-finalize-orphan (MR9) that the one-shot cargo xtask sweep-finalize-orphans drives to shred-or-quarantine pre-saga orphaned finalize graphs (runbook).

canopy-applications

ACA §1413 single-streamlined intake with expedited SNAP screening, per-program processing deadlines, typed intake sections with a completeness gate, authorized-representative CRUD, applicant-portal credential verification (POST /v1/applicants/verify-credential, portal-only since #1441, ADR-026), client-side-encrypted Apply-form drafts (POST /v1/applicants/drafts create-draft + PATCH /v1/applicants/drafts/{id} patch-draft + GET /v1/applicants/drafts/{id} get-draft for resume (#727; returns the blind ciphertext blob the passcode-holding client decrypts, 404 once expired), all portal-only since #1441 (portal:intake); ciphertext the server cannot read at rest, reserved-id lifecycle), and Apply-form finalize (POST /v1/applicants/drafts/{id}/finalize, MR6c) — the materialise step that creates persons → household → income over the canopy-persons service API (ADR-019 service token) and inserts the applications row with the reserved id + deletes the draft in one transaction — running default-on as the ADR-038 recoverable finalize saga since epic &71 MR8 (receipt-tagged idempotent persons writes, lease-fenced claim, pinned per-generation inputs, held→released events; finalize_saga_enabled: false opts back to the legacy path). A daily background reaper (the run_with_advisory_lock leader-election pattern, MR6d) clears expired drafts + their reserved credentials, exposed on demand via POST /v1/applicants/drafts/reap (service-caller only). The lost-credential recovery backend (MR8a, #634; applicant-portal design ref §3.4-3.8) adds POST /v1/applicants/recover/initiate (App-ID gate → confidential/locked short-circuit → DOB second factor against canopy-persons → a 24h pending recovery with a kill-switch; always 200 with the outcome in the body so it is not an enumeration oracle) and POST /v1/applicants/recover/kill/{token} (the "this wasn’t me" cancel that locks the case via applications.recovery_locked), gated by the applications.confidentiality flag; a second recovery-pruner leader-elected tick housekeeps the recovery_pending rows. A third leader-elected tick — the ADR-038 finalize reconciler (5-minute cadence, MR7) — compensates stuck finalize operations through the canopy-persons cancel surface (held events dropped, exclusively-finalize entities crypto-shredded, shared ones quarantined), retries unconfirmed post-commit event releases until downstream can see the committed finalize, prunes terminal saga rows past retention (never a completed-but-unreleased one), and alarms on releases failing past grace (runbook). An internal GET /v1/applicants/recover/{recovery_id} (MR8c, service-caller only) returns the application-time contact + the kill-switch token for the canopy-notices subscriber — they live on the row, off the event (ADR-004). Since ADR-042 (#1006) applicant uploads QUARANTINE at scan_status='pending' and a fenced scan-promotion worker (clamd sidecar by default; the documents table is the queue) settles content-identity-bound verdicts; content/accept/reject gate 409-unless-viewable, skipped carries an audited supervisor override (POST …/scan-override), and POST …/rescan is the supported terminal-row recovery (CLI: canopy application document-rescan). Publishes application.submitted, application.expedited_identified, application.withdrawn, application_section.updated, application_section.completed, application.applicant.recovery_{initiated,killed,confidential_blocked}, application_document.{scan_completed,scan_overridden,acceptance_revoked,scan_requeued}.

canopy-eligibility

Orchestrator. Dispatches determination requests to program services in parallel via a ProgramServiceRegistry, applies per-program circuit breakers (5 failures / 60s recovery), verifies each JWS signature, and assembles a combined result with approved/denied/pending categorization. Also serves worker-dashboard feeds: cross-program alerts (#523), case-status badges, and a per-household determination read-through. Publishes determination.completed.

canopy-verification

Federal-hub adapter service. IevsAdapter (state wage, UI, SSA SDX/BENDEX) and SaveAdapter (DHS immigration status) traits, each with a deterministic Noop implementation for UAT and stub live adapters pending credentials/agreements. Internal endpoints (X-Service-Api-Key) accept IEVS/SAVE match requests; domain endpoints surface pending verification work items and unreviewed IEVS hits to the worker dashboard (#519, #522). Owns the verifications and ievs_hits tables.

canopy-enrollment

Creates SNAP enrollments from approved determinations and runs the EBT issuance pipeline (EbtAdapter with NoopEbtAdapter): first-month proration (7 CFR 274.2(b)), 30-day/7-day-expedited initial issuance (7 CFR 273.2(i)), and 12-month stale-benefit expungement tracking. The household-scoped issuance reads (GET /v1/households/{id}/issuances for overpayment math + GET /v1/households/{id}/annual-summary for the applicant Home "Your year" recap, #719) share a #408 Pub 1075 AC-6 least-privilege gate + read-audit (supervisor/admin or an active assignment for the resolved worker; a bare service read is allowed + unaudited — since #1441 the portal reaches only the annual summary, on its scoped citizen-class arm, and is 403 on issuances). Since epic &72 it also owns the SNAP adverse-action pipeline (the action entity + its policy snapshot): schedule/cancel APIs, per-appeal stays with monotonic links, the guarded enact sweep + on-demand trigger (#1102), and the narrow Chart 3730.1 periodic-report reopen (#1108). Publishes enrollment.created, enrollment.benefits_issued, enrollment.expungement_pending, and the action lifecycle events enrollment.adverse_action_{scheduled,terminated,vetoed,cancelled}; subscribes to determination.completed.snap for auto-enrollment, notice.generated / notice.dispatched (the enact-gate dispatch evidence), appeal.decision_recorded / appeal.withdrawal_finalized (appeal convergence), and renewal.snap_periodic_report_processed (completion tombstones).

canopy-renewals

SNAP certification periods (12-month standard / 24-month elderly-disabled), 6-month interim contacts, change reports with redetermination flags, and a daily renewal scheduler (on-demand trigger POST /v1/renewals/scheduler/run, #1109). Also owns the PAMMS 3730 periodic-report calendar (#1106–#1108): cycle materialization, the two-notice drains (15th-of-prior-month pr-due, 5th-of-due-month combined notice + termination action), the veto/cancel re-determination worker queue, and the Chart 3730.1 30-day reopen. #1218 adds the snap_caseload_daily caseload-depth rollup — its own window-fenced daily job feeding GET /v1/renewals/caseload-trend at O(buckets) render cost, with an honest-503 freshness contract and an on-demand trigger (POST /v1/renewals/caseload-rollup/refresh). Program-parameterized interim-contact / change-report routes serve TANF/Medicaid/CAPS/WIC; an overdue feed supports the cross-program dashboard panel (#520). Publishes renewal.snap_due, renewal.snap_interim_contact_due, renewal.snap_certification_created, renewal.material_change, renewal.snap_periodic_report_due, renewal.snap_periodic_report_processed; subscribes to the enrollment.adverse_action_{terminated,vetoed,cancelled} terminal events (periodic-report closures / re-determination routing).

canopy-notices

Typst-based PDF generation (ADR-010) on a dedicated render thread, with 14 SNAP templates (10 notices + 4 forms) built on the Orchard design system. Also the project’s general signed-document renderer (ADR-029): POST /v1/documents/render renders any allow-listed (non-NOA) template from free-form JSON inputs and optionally ES256-signs it (detached JWS over the canonical inputs → X-Canopy-Signature + embedded in the PDF); the audit "Cite for hearing" citation is its first non-NOA consumer. Config-driven event→notice routing via notices/manifest.toml — adding a program/event is a TOML edit plus a Typst template, no Rust change; since #1107 the table also discriminates on the payload’s created_source (a periodic_report-sourced adverse action routes to the 3730 pr-combined letter, renewal.snap_periodic_report_due to the informational pr-due). Advance-notice enforcement (Georgia 14 days / federal min 10); since #1101 notices persist their adverse-action binding (adverse_action_id / generation / reason code / exemption authority, the typed effective_date_policy, cb_election_deadline) so the evidence events carry the spine id back to enrollment. Event-routed generation is a durable work-item queue (#1091, epic &72): the subscriber enqueues atomically with its inbox row, a worker resolves the REAL recipient from canopy-persons (mailing-first, redacted/incomplete rejected) before rendering, render failures block-and-retry (no PDF-less rows), and a dispatcher stamps dispatched_at dispatch evidence — see the API page. Read tracking (#721): POST /v1/notices/{id}/mark-read sets a nullable read_at (idempotent — first-read time preserved) so the applicant-portal Letters inbox can show read/unread; since #1442 the service itself enforces the household binding against the citizen’s signed ownership claim (uniform 404 on mismatch, before the stamp), with the BFF’s own pre-check as defense-in-depth. Publishes notice.generated, notice.dispatched (#1091); subscribes per jurisdiction manifest. A separate canopy-notices.recovery subscriber (Plan 3 MR8c, ADR-026) handles application.applicant.recovery_initiated as an email/SMS side-channel (not a PDF): it reads the application-time contact + the kill-switch token back from canopy-applications (the event carries IDs only, ADR-004) and delivers the one-tap "this wasn’t me" kill link + the 24h reveal time — never the passcode. UAT delivery is a logging stub (contact redacted).

canopy-exchange

FFE account-transfer integration (Georgia Access). FfeAccountTransferAdapter trait is defined but has no methods — implementation is blocked on the federal partner. Stub service (healthz + metrics only).

canopy-appeals

Fair hearings plus the IPV/ADH disqualification workflow. Appeals of adverse actions are action-bound (#1098, epic &72): the filing carries the enrollment adverse_action_id, the server stamps the filing date, and continued benefits follow the Chart B2 election — a timely election takes a synchronous fenced stay on the action before the grant commits (pending_stay + retry worker when enrollment is down; never granted without a stay receipt). The 60-day decision SOP (7 CFR 273.15(c)(1), extendable by ONE recorded household postponement — #1099 corrected the prior 90-day figure, which is the FILING window) runs a daily background check; the penalty calculator applies 12-month/24-month/permanent disqualification (7 CFR 273.16(e), trafficking always permanent) with cross-program prior-offense counting. Publishes appeal.filed, appeal.continued_benefits_granted, appeal.decision_recorded, appeal.withdrawal_finalized (#1102 — the pinned Phase-2 payloads, activated; the legacy decision_issued/decision_reversed pair is deleted), appeal.overpayment_assessed, appeal.overpayment_assessment_voided (#1105 — retires an assessment with a possible downstream claim), appeal.decision_deadline_approaching, appeal.overdue; subscribes to snap.overpayment_claimed (the claim-acks consumer, #1105 — flips the referenced assessment computed → applied). On an ADH that finds no IPV it also publishes ipv.not_established (#981) so a program service reprocesses the over-issuance as a non-fraud inadvertent-household-error claim (7 CFR 273.16(e)(8)).

canopy-reporting

Federal reporting and cross-program roll-ups, assembled entirely via HTTP from the owning services (ADR-001 — no direct DB access). SNAP: FNS-388 monthly participation + FNS-7176 QC 24-column CSV. TANF: ACF-199 enriched extract (work hours, sanctions, time limits), WPR (all-family + two-parent per 45 CFR 261.21), ACF-196 quarterly stub. Medicaid: T-MSIS with 38-COA→coverage-group mapping, CMS-64 enrollment aggregation, CMS-416 EPSDT by age band. Cross-program: overpayment recovery roll-up CSV. All report surfaces are user-only under the OIDC receiver contract (#1438): supervisor RBAC carried by the worker’s exchanged aud=canopy-reporting token — service-class and (under enforcement) direct broad-audience worker bearers are 403; the runs reads + overpayments summary stay dual.

canopy-security

Audit and compliance sink. A wildcard (#) subscriber captures every event system-wide and persists hash-chained audit_events; a best-effort POST /v1/security/audit/ingest endpoint (service-or-portal since #1441: portal:audit:write) adds the same hash-chained ingress over HTTP for broker-less services such as the applicant portal (ADR-026). Surfaces breach alerts, NIST SP 800-53 control mappings, chain verification, and archival of events past their retention window. See also Security Operations.

canopy-snap

SNAP eligibility (gross/net income tests, six mandatory deductions, allotment), ABAWD work-requirement tracking, categorical eligibility (standard CE / BBCE / student exclusion), IEVS verification surfaces, alien eligibility, a parameters API, and overpayment claims/plans/recoupments (PAMMS 9000 / 7 CFR 273.18). All eligibility logic runs through canopy-rules (ADR-003); federal params load from rulesets/federal/ at startup. Publishes determination.completed.snap, snap.case_closed (#651 — emitted on a denied determination, SNAP’s case-closure signal; drives the canopy-medicaid ELE source-closure lapse, mirroring tanf.case_closed), abawd.warning_month_1, abawd.warning_month_2, abawd.time_limit_reached, and snap.overpayment_claimed (→ the 7 CFR 273.18 overpayment demand notice). Subscribes to ipv.not_established (#981 — an ADH finding no IPV opens a non-fraud inadvertent-household-error claim + the demand notice), appeal.overpayment_assessed (continued-benefits-on-appeal claim), and tanf.case_closed (transitional SNAP).

canopy-tanf

TANF eligibility determination including the PAMMS 1351 sanction gate and PAMMS 1345–1370 personal-responsibility gate (emitting DeterminationStatus::Sanctioned), FTI audit logging (Pub 1075, ADR-014), work requirements, time limits, SSA data, three JDM rulesets, the work-activity list/summary (the ACF-199 WPR source of truth), and overpayment claims/plans/recoupments (42 USC 609(a)(1); 45 CFR 263.11). Owns a tanf_discrepancies table with a worker-portal resolve endpoint (#448). Publishes tanf.case_closed, tanf.application_approved.

canopy-medicaid

All 38 classes of assistance (COAs) evaluable via four JDM rulesets — MAGI, non-MAGI ABD, non-MAGI family, CHIP (PeachCare). CMD cascade evaluates COAs in PAMMS 2052 priority order; the EE15 hierarchy is an orchestrator-propagated 38-COA priority chain. Q-Track income+resource tests, MN spenddown, TMA (Phase 1/Phase 2 with a tanf.case_closed subscriber). FTI audit (Pub 1075, ADR-014), 165 policy citations, and overpayment claims/plans/recoupments (42 CFR 433.300). Worker-portal CMD ingest + determination requeue endpoints (#448). Subscribes to tanf.case_closed, snap.application_approved, tanf.application_approved (Express Lane — when an approval arrives before ELE consent, #649 defers it to ele_deferred_approvals and the ele-consent subscriber replays the grant when consent lands, so the durable grant is not lost to event ordering), snap.case_closed (#651 — a dedicated ele-case-closed-snap group running the ELE source-closure lapse for SNAP, parallel to the TANF path that shares the tma group), and persons.income_changed (#652 — the ele-income-changed group routing income_change through ele-lapse-2026; durability-biased keep federally).

canopy-caps

CAPS/CCDF eligibility: income (initial 50% / continued 85% SMI), activity requirement (24 hrs/week), age gate (<13, or <19 for special needs), sliding-scale copayment, and a 12-month provider authorization with a CAPS provider registry (#396, FK-validated; soft-delete preserves historical authorizations; FK/unique violations surface as HTTP 422). Publishes caps.determined, caps.authorization_created.

canopy-wic

WIC eligibility: five participant categories, income (185% FPL), adjunctive eligibility (SNAP/Medicaid/TANF), a clinical nutritional-risk gate (recorded, not computed), food-package families (7 CFR 246.10(e)(1)-(7)), and certification periods (7 CFR 246.7(g)); per-participant determinations (one signed envelope per participant, ADR-035 / #769). Worker-portal appointment scheduling + an upcoming-appointments dashboard feed (#448, #521). Publishes wic.determination_completed, wic.certification_created.

canopy-web (worker portal)

Worker-facing BFF. Orchard theme system (jurisdiction-swappable colors/logo via theme.toml, 3-way Light/Dark/System toggle), local vendor files under strict CSP (htmx 2.0.4, Alpine.js CSP build — no CDN, no inline scripts), WorkerRole-aware extractors, eight internal service clients with a 5s timeout for graceful degradation, and WCAG 2.1 AA affordances. The system-wide AuditLog (/audit-log, Admin/StudioAdmin/Auditor) carries a master-detail rail whose "Cite for hearing" action streams a signed PDF citation (GET /audit-log/citation/{id}/pdf orchestrates canopy-security event+chain → canopy-notices render+sign, ADR-029). The worker action surface includes the #1103 SNAP adverse-action schedule/cancel-termination actions (POST /actions/snap/{schedule,cancel}-termination — policy-vocabulary reason codes, operator-tier advance-notice exemption, server-side enrollment/action resolution). Composition runtime per ADR-021/ADR-022.

canopy-portal (applicant portal)

Applicant-facing BFF (ADR-008 + ADR-026). Per ADR-026 the portal is Postgres-free with Redis-primary sessions; the privacy-first Dioxus app is shipped (Plan 3 complete). Fluent i18n (en + es) is wired. It is built via its own Dioxus dx pipeline (services/canopy-portal/Dockerfile, a glibc image — not the musl monorepo image) and runs as a containerized devstack service on port 8090 (#659), like every other service. See canopy-portal Fluent i18n.

Sessions are opaque tokens in a dedicated noeviction Redis keyspace (the redis-sessions devstack container, distinct from the allkeys-lru cache — live sessions must never be evicted, and Redis’s eviction policy is instance-global). The SHA-256 hash of the token is the Redis key (session:{hash}); the raw token lives only in an HttpOnly + SameSite=Strict cookie, and TTL is flow_kind-derived. Three plain Axum routes (mounted before the Dioxus fallback, not under the canopy-api /v1 JWT group) carry the authenticated flow: POST /lookup/submit (a sub-path — the GET /lookup page is the Dioxus SPA, so a POST /lookup would shadow it; #659) mints the portal’s NARROW per-target ADR-019 token (#1440: one scope-aware source per backend target, aud-canopy-<target> exactly — the broad audience is gone), verifies the applicant’s HH-… code + passcode against canopy-applications, mints a session + fire-and-forgets a session-mint audit event to canopy-security; GET /me reads the session; POST /logout revokes it (the kill-switch). The portal is served from its release dx build in the devstack (#659): a GET /readyz readiness probe (200 only when the applicant routes are mounted and the applications-target narrow token can be acquired — not the always-ok /healthz) gates the compose healthcheck, and a portal-csp Playwright project asserts the strict CSP (enforce-mode header + nonce policy + no 'unsafe-inline' + zero violations) on the public pages against the live served build.

The Apply flow (Plan 3 MR6b) is the privacy-first incremental application: the WASM client encrypts each step’s payload locally — Argon2id derives a 256-bit key from the applicant’s passcode + the server’s kdf_salt, and XChaCha20-Poly1305 (24-byte nonce from crypto.getRandomValues) seals it (crate::crypto; no getrandom wasm backend, no new CSP directive). Two more plain Axum routes carry it: POST /apply/start calls canopy-applications create-draft, mints a FlowKind::Apply session bound to the reserved id, and returns the credential so the client can derive the key (the passcode is not stored in the session — the server keeps no key material); POST /apply/save reads the session → the reserved id and forwards the client’s {ciphertext, nonce, enc_version, current_step} to patch-draft for that id (the id comes from the server-trusted session, never the client). The client→server call is a CSP-clean same-origin fetch (connect-src 'self'; the portal stays reqwest-free on the client). On submit (MR6e), POST /apply/finalize reads the session → reserved id and forwards the client’s FinalizeRequest (built from the in-memory plaintext) to canopy-applications finalize with the service token; the wizard then reveals the Application ID + passcode client-only (never server-rendered — the passcode never reaches server HTML or logs). Resume (#727): the credential is surfaced at start (the "save your Application ID" screen right after Begin) so a real applicant can leave and come back — and a "Continue your application" affordance on the apply intro POSTs the typed code + passcode to POST /apply/resume, which verifies the credential (→ the reserved id, the IDOR boundary), fetches the encrypted blob via get-draft, and mints a fresh Apply session; the client then re-derives the Argon2id key from the passcode it still holds, decrypts the blob, and hydrates the form at the saved step (a wrong credential is 401, a missing/expired draft is 404). The full apply→submit and resume flow is live.

The write endpoints (/lookup + /apply/) carry a rate-limit cascade (Plan 3 MR7, src/ratelimit.rs, applicant-portal design ref §3.6) over the same noeviction redis-sessions keyspace (rl: namespace). A from_fn middleware runs the device-cookie tier (60/hr, 200/day — the cascade’s primary key, *not IP, so CGNAT neighbours don’t punish each other; a 1-year HttpOnly device cookie is issued if absent; sized above one applicant’s autosave write-count — /apply/start + per-step /apply/save + /apply/finalize ≈ 6 — so the wizard’s own per-step saves can’t false-trip a legitimate finalize, raised from the original 5/hr·10/day in Plan 3 MR11c) and the IP tier (300/hr; 3000/hr on the RFC 6598 100.64.0.0/10 CGNAT block), rejecting with a uniform 429 + Retry-After that never names which tier tripped; the per-CaseID "8 wrong/day" brute-force cap runs in the /lookup handler (reject is indistinguishable from a wrong passcode — oracle-safe — and caps progress even across cycled devices/IPs). The limiter fails open on a Redis error (a protection layer, not a correctness gate). The recovery backend it guards landed service-side in MR8a (POST /v1/applicants/recover/* on canopy-applications); the portal /recover wizard (MR8b) adds a config-driven CAPTCHA verifier abstraction shipping noop by default — the real provider (preferred org-hosted mCaptcha proof-of-work service, fallback hCaptcha/Turnstile, never reCAPTCHA) and any provider-derived CSP carve-out are deferred to a follow-up issue, so the cascade is the live recovery-abuse control in the interim.

Developer tooling

Tool Description

canopy-seed

Deterministic seed-data generator (--seed for reproducibility, --households for size). Invoked via cargo xtask seed. Its cast of login-capable applicants (phase14_cast) is the demo surface — one seed, no separate profile.

cargo xtask

Workspace task runner — build/test/validate, devstack lifecycle, migrations, policy audit, rules check, demo verification. See CLI Reference.

Currency

This catalog is overview-level by design: it carries the service topology and cross-cutting concerns that have no per-service home, plus a one-paragraph capability summary per service. Endpoint and schema detail are not duplicated here — they live in the linked API Reference and Data Model pages, which are the canonical sources and are refreshed from utoipa snapshots and migration SQL respectively. When a feature MR changes a service’s role, events, or topology, update the relevant block here in the same MR (per the doc-sweep discipline); when it only changes endpoints or tables, update the API/Data Model page instead.

Edit this page · default