canopy-applications Data Model
On this page
Cross-link: canopy-applications API Reference · Source: migrations/
Tables
| Table | Purpose |
|---|---|
|
Root intake record. One row per submission. Carries |
|
Per-program child rows on each application. One row per (application, program); a single application requesting SNAP + Medicaid + TANF creates three rows. Carries the per-program |
|
Per-program intake-section storage. One active row per |
|
Authorized-rep records per household (7 CFR 273.1(f), 45 CFR 162). One row per |
|
Per-worker case-assignment table. Sole source of truth across services that gate household-scoped reads on assignment (canopy-enrollment first; canopy-renewals / canopy-notices / canopy-reporting on adoption). Pub 1075 AC-6 least-privilege baseline. Soft-delete via |
|
Applicant-facing Application ID code ( |
|
argon2id hash of the applicant’s 12-digit passcode ( |
|
Client-side-encrypted Apply-form draft — the server stores ciphertext it cannot bulk-read. Keyed on the reserved |
|
24-hour pending lost-credential recovery state. One active row per application (partial unique index |
|
Uploaded applicant/worker documents. The bytes live in object storage (canopy-store) keyed by |
|
Durable finalize-saga record — one row per reserved application id (= |
|
Local skip-cache of the persons-side finalize receipts: |
|
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) ( |
Cross-service FKs (ADR-001 boundary)
Per ADR-001, canopy-applications holds no Postgres-level foreign keys to other services. The only DB-level FKs in this schema are intra-database: application_programs.application_id → applications(id) and the in-row authorized_representative_id self-reference. Every UUID column marked FK → canopy-persons in the ERD (household_id, submitted_by, representative_person_id, worker_id) is an application-level foreign key — canopy-applications trusts the orchestrator (canopy-eligibility) and the BFFs to supply real IDs, but does not enforce existence. application_programs.determination_id is a cross-service ID pointing into the relevant program service’s database (canopy-snap / canopy-tanf / canopy-medicaid / canopy-caps / canopy-wic) and is likewise application-level only.
Retention
canopy-applications does not hold FTI or PHI; no Pub 1075 / HIPAA retention floor applies at this layer. 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) requires 3-year minimum; Medicaid (42 CFR 431.17) requires 3-year minimum from claim closure. 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
-
applications_household_idx— household lookup -
applications_status_idx(partial,WHERE active = true) — status filter on caseload search -
applications_submitted_by_received_at_idx(partial,WHERE active = true, #402) — caseload search by submitter ordered most-recent-first -
applications_received_at_idx(partial,WHERE active = true, #402) — all-applications listing ordered most-recent-first -
application_programs_unique(unique, partial,WHERE active = true) — enforces one active row per(application, program) -
application_programs_program_active_idx(partial,WHERE active = true, #402) — supports the program-filter EXISTS clause in caseload search -
application_sections_unique(unique, partial,WHERE active = true) — enforces one active section row per(application_id, program, section_name) -
application_sections_app_program_idx(partial,WHERE active = true) — per-application/program section listing -
application_sections_last_edited_by_idx(partial,WHERE active = true) — worker-attribution lookup -
auth_reps_household_idx(partial,WHERE active = true) — household lookup of active representatives -
household_assignments_active_uniq(unique, partial,WHERE unassigned_at IS NULL) — enforces one active assignment per(worker, household) -
household_assignments_by_worker_active(partial,WHERE unassigned_at IS NULL) — worker caseload -
household_assignments_by_household_active(partial,WHERE unassigned_at IS NULL) — reverse lookup ("who owns this household") -
application_id_codes_code_idx(Plan 3 MR4) —codelookup for the/lookupresume/login path -
passcode_hashes_active_per_app(unique, partial,WHERE revoked_at IS NULL, Plan 3 MR4) — one active passcode hash perapplication_id; revoked rows persist for audit -
application_drafts_expires_idx(Plan 3 MR6) —expires_atscan for the sliding-window reaper (WHERE expires_at < now()) -
recovery_pending_active_per_app(unique, partial,WHERE killed_at IS NULL AND completed_at IS NULL, Plan 3 MR8a) — one active recovery per application (absorbs re-initiation; no duplicate notification) -
recovery_pending_reveal_due_idx(partial,WHERE completed_at IS NULL AND killed_at IS NULL, Plan 3 MR8a) —reveal_atscan for the reveal screen + the pruner’s staleness sweep -
application_documents_app_idx((application_id, uploaded_at DESC), Plan 3 MR9a) — the case-detail Documents list query (newest first) -
application_documents_person_idx((person_id), Plan 3 MR9a) — per-person document lookup -
application_documents_pending_idx(partial,(application_id, document_type) WHERE accepted_at IS NULL AND rejection_reason IS NULL, Plan 3 MR9a) — outstanding (un-reviewed) documents per application -
application_documents_scan_due_idx(partial,(scan_due_at, id) WHERE scan_status = 'pending', ADR-042) — the scan worker’s deterministic claim order -
finalize_operations_stuck_idx(partial,(state, lease_expires_at) WHERE state IN ('in_progress','compensating'), epic &71 MR4) — the reconciler’s scan for lease-lapsed / mid-compensation operations -
finalize_operations_unreleased_idx(partial,(state) WHERE state = 'completed' AND events_released = false, epic &71 MR4) — the release-retry scan for committed apps whose persons events are still held -
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_applications_tables.sql— original schema (applications,application_programs,authorized_representatives) including interview / expedited-screening / submission-channel columns -
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 -
20260510000000_add_list_filter_indexes.sql— composite + partial indexes for the caseload-search query shape per #402 -
20260511000000_create_household_assignments.sql— #408 per-worker assignment table for cross-service household RBAC (Pub 1075 AC-6 least-privilege) -
20260516000000_create_event_inbox.sql— #433 consumer-side inbox (ADR-018 amendment) -
20260601000000_create_application_sections.sql— worker-intake program-independence Plan MR2 per-program intake-section storage (application_sections) -
20260601000001_extend_applications_status.sql— worker-intake program-independence Plan MR2applications_status_check+application_programs_status_checkCHECK constraints (advisory-lock-guarded expand-only backfill) -
20260602000000_create_application_id_codes.sql— Plan 3 MR4 applicant Application ID code table (application_id_codes; reserved-id, noapplicationsFK per ADR-026) -
20260602000001_create_passcode_hashes.sql— Plan 3 MR4 argon2id passcode hash table (passcode_hashes; reserved-id, partial-unique active row, rotation-friendly) -
20260602000002_extend_applications_notify_columns.sql— Plan 3 MR4notify_email/notify_phone_e164onapplicationsfor the recovery side-channel (applicant-portal design ref §3.7) -
20260603000000_create_application_drafts.sql— Plan 3 MR6 client-side-encrypted Apply-form draft table (application_drafts; reserved-id, noapplicationsFK per ADR-026; sliding 30-dayexpires_at) -
20260604000000_extend_applications_recovery_gate.sql— Plan 3 MR8a (#634)confidentiality(CHECK-set) +recovery_lockedboolean onapplications— the lost-credential recovery gate (applicant-portal design ref §3.8) -
20260604000001_create_recovery_pending.sql— Plan 3 MR8a 24h pending-recovery state (recovery_pending; FKsapplications; active-per-app partial unique index; kill-switch token) -
20260605000000_create_application_documents.sql— Plan 3 MR9a applicant/worker document uploads (application_documents; FKsapplications;document_type/document_kind/scan_status/uploaded_by_sourceCHECK sets; per-app + per-person + pending-review indexes) -
20260722000001_document_rejected_by.sql— #1009:application_documents.rejected_by UUID(the rejecting reviewer, from the verified actor claim) -
20261106000000_document_scan_quarantine.sql— ADR-042 (#1006): the quarantine lifecycle — provenance/worker/override columns, the seven state-machine CHECKs,'pending'default, the staggered all-legacy backfill (every pre-quarantine verdict was unprovable; acceptances cleared), the scan-due partial index -
20260714000000_create_finalize_operations.sql— epic &71 MR4 (ADR-038) durable finalize-saga record (finalize_operations; reserved-id, state machine + fencing lease + pinned inputs CHECKs; stuck + unreleased partial indexes) -
20260714000001_create_finalize_steps.sql— epic &71 MR4 (ADR-038) local receipt skip-cache (finalize_steps; PK(application_id, generation, step_key); FKfinalize_operationsCASCADE) -
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
All migrations are forward-only per ADR-016.