canopy-eligibility Data Model
On this page
Cross-link: canopy-eligibility API Reference · Source: migrations/
Tables
| Table | Purpose |
|---|---|
|
Root orchestration record. One row per cross-program eligibility run, keyed by |
|
Per-program signed-determination record. One row per program service response within a run; |
|
Assembled cross-program result for one run. One row per |
|
Per-service transactional outbox (ADR-018), schema single-sourced in |
|
Per-service consumer inbox (#433 / ADR-018 amendment). |
Cross-service FKs (ADR-001 boundary)
Per ADR-001, canopy-eligibility holds no Postgres-level foreign keys to other services. The only DB-level FKs in this schema are intra-database: program_determinations.eligibility_request_id → eligibility_requests(id) and combined_results.eligibility_request_id → eligibility_requests(id). Every UUID column that points outside this database — eligibility_requests.application_id (→ canopy-applications), eligibility_requests.household_id (→ canopy-persons), and the same application_id / household_id carried on program_determinations and combined_results — is an application-level foreign key only; canopy-eligibility trusts the caller and the BFFs to supply real IDs but does not enforce existence. Per ADR-002 the orchestrator never persists raw program inputs: program_determinations stores only each program service’s signed, JWS-verified determination envelope, not the data that produced it.
Retention
canopy-eligibility does not hold FTI or PHI; no Pub 1075 / HIPAA retention floor applies at this orchestrator layer. The signed determinations it persists are program-output envelopes (ADR-002 black-box contract), not raw program data, so the FTI/PHI floors live in the program services that own the underlying records (canopy-tanf, canopy-medicaid). SNAP record retention (7 CFR 272.1(f)) requires 3-year minimum for case records; TANF (45 CFR 75.361, the HHS uniform-administrative-requirements floor — no dedicated TANF retention CFR exists) and Medicaid (42 CFR 431.17) likewise require 3-year minimums. The longest applicable floor governs in deployments that run multiple programs. Retention is operator-driven (archive moves, not migration-driven destructive changes — ADR-016 forward-only).
Indexes
-
idx_eligibility_requests_application— request lookup by application -
idx_eligibility_requests_household— request lookup by household -
idx_eligibility_requests_status— status filter on the orchestration queue -
idx_unique_pending_request(unique, partial,WHERE status IN ('pending','in_progress'), migration 20260402000000) — prevents duplicate concurrent runs for the same(application_id, household_id) -
idx_program_determinations_request— per-run determination fan-in -
idx_program_determinations_program— program filter across determinations -
idx_program_determinations_application— determination lookup by application -
idx_program_determinations_household_determined_at(migration 20260804000000) — newest-first case-status lookup per household (#1196; the history is append-only and unbounded, so this read must stay index-scan class) -
idx_program_determinations_household_program_determined_at(migration 20260804000000) — the #694 program-scoped case-status variant -
idx_program_determinations_alert_status(partial,WHERE status IN (denied, sanctioned, time_limit_exceeded, disqualified, terminated, abawd_exceeded), migration 20260804000000) — cross-program alerts panel feed; the predicate must stay in lockstep withlist_recent_alert_determinationsor the planner stops substituting it (pinned by the EXPLAIN regression test indetermination_index_scan_test.rs) -
idx_combined_results_request— combined result by run -
idx_combined_results_application— combined result by application -
idx_combined_results_household— combined result by household -
event_outbox_*— the three generated partial outbox indexes (drainer hot path, lease-aware claim, ADR-039 held-skip); documented once in the data-models index -
event_inbox_unprocessed_idx(partial,WHERE processed_at IS NULL) — replay / janitor hot path
Migration files
-
20260326000000_create_eligibility_tables.sql— original orchestrator schema (eligibility_requests,program_determinations,combined_results) plus base lookup indexes -
20260402000000_add_constraints.sql—eligibility_requests_status_checkclosed-set status constraint +idx_unique_pending_requestpartial unique index (test-coverage-audit hardening) -
20260508000000_create_event_outbox.sql+20260518004851_event_outbox_lease_columns.sql+20260713000000_event_outbox_hold.sql— the generated ADR-039 single-sourced outbox migrations (cargo xtask outbox-migrations --write); documented once in the data-models index -
20260516000000_create_event_inbox.sql— #433 consumer-side inbox (ADR-018 amendment) -
20260804000000_pd_lookup_indexes.sql— the three #1196program_determinationslookup indexes. Deliberately transactional (NOTCONCURRENTLY): sqlx serializes concurrent migrators on a per-database advisory lock, and a second booting replica blocks on it holding the very snapshotCONCURRENTLYwaits out — a reproduced hard deadlock whose victim can be the build itself, stranding an INVALID index behindIF NOT EXISTS. Operators upgrading an already-huge live table pre-create the same indexesCONCURRENTLYout of band (verifypg_index.indisvalid), and the migration no-ops
All migrations are forward-only per ADR-016.
Cohort-run substrate (#1213, migration 20261120000000_cohort_runs.sql)
| Table | Purpose |
|---|---|
|
One row per bulk run: state machine ( |
|
One row per cohort member: |
|
THE durable ledger (B2/B4): |
|
Append-only attempt audit: real attempts carry a partial-unique ordinal per
|
|
The H22 operator ledger: actor, actual privilege, reason, from/to states, typed detail. Immortal. |
eligibility_requests gains bulk_case_id/bulk_program_epoch (+ a live-rows partial unique per
case); program_determinations gains nullable trigger + policy_target.