canopy-renewals Data Model
On this page
Cross-link: canopy-renewals API Reference · Source: migrations/
Tables
| Table | Purpose |
|---|---|
|
One row per SNAP certification period. Holds the |
|
Mid-certification change report ledger. One row per change reported by the household (or system-detected). Carries |
|
One row per mid-certification reported fact change that the materiality subscriber re-scored. When a worker authors an income/asset/expense/member fact during an active SNAP certification, canopy-renewals runs a non-persisting materiality dry-run (canopy-eligibility) against the determination-of-record’s frozen policy + pinned corpus and records the verdict diff here: |
|
One row per periodic-report CYCLE on a certification (PAMMS 3730, eff. June 2026; 7 CFR 273.12(a)(5)(iii)). |
|
The veto/cancel re-determination worker queue: one row per DEAD periodic-report adverse action ( |
|
Per-service transactional outbox (ADR-018), schema single-sourced in |
|
Per-service consumer inbox (#433 / ADR-018 amendment). |
|
Wall-clock window fence for the service’s daily scheduler tick(s) ( |
|
The caseload-depth rollup — sole feed of |
Cross-service FKs (ADR-001 boundary)
Per ADR-001, canopy-renewals holds no Postgres-level foreign keys to other services. The columns marked FK → canopy-persons / FK → canopy-applications / FK → canopy-snap above are application-level foreign keys: canopy-renewals trusts the upstream services to supply real IDs but does not enforce existence in canopy_renewals. Cross-service IDs the service holds are household_id, application_id, determination_id, renewal_application_id, renewal_determination_id, redetermination_application_id, and processed_by.
Retention
SNAP renewal certification records are governed by 7 CFR 273.14 (recertification) and 7 CFR 274.6 (case records — 3 years from last activity). snap_certifications rows are retained for the full 3-year window post-cert-end-date; snap_change_reports rows inherit the same retention from their parent certification. Rows are not deleted in production.
Indexes
-
idx_snap_certs_household— household-scoped cert lookup -
idx_snap_certs_status— list-by-status (recertification queue, expired sweep) -
snap_certs_one_active_per_household(partial UNIQUE,WHERE status = 'active' AND active = true) — at most one active certification per household (#973) -
idx_snap_certs_end_date— renewal-notice scheduler scan -
idx_snap_certs_end_date_id(compound,(certification_end_date ASC, id ASC)) — theGET /v1/renewals/snap/duekeyset order + cursor (#1204); supplies the(end_date, id)total order the single-columnidx_snap_certs_end_datecould not, removing the top-N Sort -
idx_snap_certs_interim_due(partial,WHERE interim_contact_due_date IS NOT NULL) — interim-contact queue -
idx_snap_certs_renewal_due_unnotified(partial,(certification_end_date ASC, id ASC) WHERE renewal_notice_sent_date IS NULL, #1198) — the scheduler’s fenced renewal-due drain; excludes fenced rows so each daily range-scan reads only the still-unfenced (newly-due) certs in the window (bounds the per-tick scan, not the index size — the index still covers every not-yet-fenced active cert) -
idx_snap_certs_interim_due_unnotified(partial,(interim_contact_due_date ASC, id ASC) WHERE interim_contact_due_date IS NOT NULL AND interim_contact_completed_at IS NULL AND interim_contact_notice_sent_date IS NULL, #1198) — the scheduler’s fenced interim-contact drain -
idx_snap_pr_household— household-scoped cycle list (#1106) -
idx_snap_pr_due_initial(partial,WHERE status = 'scheduled') — the 15th-of-prior-month notice predicate (#1106) -
idx_snap_pr_due_combined(partial,WHERE status IN ('scheduled','notice_sent','form_incomplete')) — the 5th-of-due-month combined-notice predicate (#1106) -
idx_snap_pr_open(partial,WHERE status NOT IN ('processed','terminated','cancelled')) — the month-end closure / lapsed-VCL predicate (#1106) -
idx_snap_change_reports_cert— certification-scoped change list -
idx_snap_change_reports_household— household-scoped change list -
idx_snap_change_reports_type— change-type analytics / reporting -
idx_snap_change_reports_program— #448 program-scoped routing -
idx_snap_change_reports_household_program— #448 composite for(household_id, program)worker-portal queries -
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
-
20260401000000_create_renewals_tables.sql— original schema (snap_certifications, snap_change_reports; type / status / reporting / method CHECK constraints; cert + change indexes) -
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 -
20260512000000_add_program_to_renewals.sql— #448 multi-program support; addsprogramcolumn with DEFAULT'snap', relaxescertification_idto nullable, adds program-scoped indexes -
20260516000000_create_event_inbox.sql— #433 consumer-side inbox (ADR-018 amendment) -
20260720000000_event_inbox_parking.sql— #1089 parked-state columns +idx_event_inbox_parked(generated, single-sourced with the outbox; see the event-delivery protocol) -
20260608000000_add_certification_terminated_at.sql— addsterminated_at TIMESTAMPTZ NULLtosnap_certificationsso the caseload-depth trend (#702) reconstructs "active as of past W" losslessly across early terminations (safe additive, no backfill) -
20260626000000_create_recert_nudges.sql— T2-7 #680 recertification-nudge ledger:recert_nudgestable,UNIQUE (certification_id, source_event_id)idempotency key,triggering_fact_kind/action_takenCHECK constraints, household + pending-action partial indexes -
20260731000000_pr_reopen.sql— #1108 (epic &72 MR 5.3):snap_periodic_reports.reopened_date+sop_due_datewith the paired-travel CHECK (snap_pr_reopen_pair_check) -
20260722000000_pr_action_pipeline.sql— #1107 (epic &72 MR 5.2):snap_periodic_reports.adverse_action_id(the 5th-of-month trigger’s minted action, cross-service id) + thepr_redeterminationsworker queue (trigger/action CHECKs, per-dead-action unique, household + pending partial indexes) -
20260721000000_periodic_report_cohort_and_state.sql— #1106 (epic &72 MR 5.1): addssnap_certifications.periodic_report_requiredwith the marker-delimited P11 backfill (pre-2026-03-02 assignment, > 6-month and < 36-month duration, live rows only — the backfill-parity test re-executes the marked statement against the Rust predicate) and createssnap_periodic_reportswith the status/form/VCL-window/calendar-order CHECKs, the per-due-month + per-generation unique keys, and the three predicate partial indexes -
20260706000000_snap_cert_active_uniqueness.sql— #973 worker cert-create guards: expires pre-existing duplicate active certs (fix-forward), adds thesnap_certs_period_order_chkCHECK (end > start) and thesnap_certs_one_active_per_householdpartial unique index -
20260818000000_snap_certs_due_keyset_idx.sql— #1204 (scale audit C2): adds the compoundidx_snap_certs_end_date_id (certification_end_date ASC, id ASC)backing the keyset-paginatedGET /v1/renewals/snap/due; transactional (NOT CONCURRENTLY — sqlx-migrator advisory-lock deadlock, #1196) -
20260819000000_snap_certs_notice_fences.sql— #1198 (scale audit H1): adds theinterim_contact_notice_sent_dateemission-fence column plus the two partial drain indexes (idx_snap_certs_renewal_due_unnotified,idx_snap_certs_interim_due_unnotified) so the daily scheduler emits each renewal-due / interim-contact-due event once, not once per tick; transactional (NOT CONCURRENTLY, same #1196 rationale) -
20260905000000_create_scheduler_runs.sql— the generated #1211 window-fence table (single-sourced incrates/canopy-db/scheduler-migrations/); documented once in the data-models index -
20261103000000_snap_caseload_daily.sql— #1218 (scale audit H11): the caseload-depth rollup table (table only — deliberately NO backfill; the endpoint 503s honestly until the boot-time probe materializes)
All migrations are forward-only per ADR-016.
snap_universe_snapshots / snap_universe_snapshot_rows (#1470)
Immutable federal-universe snapshot generations (ADR-002 Amendment 1 D5 —
the #1213 bulk-cohort source). A generation header (id UUIDv7, active_on,
row_count, created_at) plus frozen member rows keyed (snapshot_id, seq)
— seq is dense 1..row_count minted at freeze time, so the PK IS the
keyset. Rows carry certification_period_id, household_id,
application_id, and determination_id (the establishing determination —
the supersession baseline). Frozen in ONE transaction with the count, so
paging to exhaustion returns exactly row_count regardless of live-table
churn. ON DELETE CASCADE is deliberate (ephemeral working sets, not
provenance): the daily fenced reaper (canopy-renewals.universe-snapshot-reaper)
deletes generations older than CANOPY_RENEWALS__UNIVERSE_SNAPSHOT_RETENTION_DAYS
(default 30).