Data Models
On this page
Canopy follows ADR-001 program-service isolation: every service owns its own PostgreSQL database. No Postgres-level cross-service foreign keys exist; cross-service consistency is the orchestrator’s responsibility (canopy-eligibility for program dispatch, canopy-applications for case-management lineage).
Per-service pages
-
canopy-snap — SNAP
-
canopy-tanf — TANF (FTI-scoped)
-
canopy-medicaid — Medicaid + CHIP (FTI + HIPAA-scoped)
-
canopy-caps — CAPS / CCDF
-
canopy-wic — WIC
-
canopy-persons — persons + households
-
canopy-applications — application intake + assignments
-
canopy-enrollment — post-determination enrollment + EBT
-
canopy-renewals — renewal certifications
-
canopy-notices — NOA generation
-
canopy-appeals — fair hearings + IPV / ADH
-
canopy-reporting — federal reporting snapshots
-
canopy-security — audit events + hash chains
-
canopy-rules — JDM ruleset CRUD
Cross-cutting tables
-
event_outbox(ADR-018 + ADR-039) — present in every service that publishes domain events; transactional outbox drained by an in-processOutboxDrainerto RabbitMQ. Eleven columns:(id, routing_key, payload, enqueued_at, published_at, attempts, last_error, claimed_at, claimed_by, hold_operation_id, hold_generation). The schema is single-sourced incrates/canopy-mq/outbox-migrations/and generated into every service (cargo xtask outbox-migrations --write); a parity gate (--check, run in the pre-push battery) fails on drift, so the same drainer + janitor works everywhere. Thehold_operation_id/hold_generationpair (ADR-039) lets a producer stage an event held (publish_tx_held) — skipped by the drainer untilrelease_held(delivered) ordrop_held(discarded); NULL for every ordinary event. Ships with four generated partial indexes —event_outbox_unpublished_idx((enqueued_at) WHERE published_at IS NULL, the drainer hot path),event_outbox_lease_idx((claimed_at NULLS FIRST, enqueued_at) WHERE published_at IS NULL, lease-aware claim ordering so replicas never double-publish),event_outbox_held_idx((hold_operation_id, hold_generation) WHERE hold_operation_id IS NOT NULL AND published_at IS NULL, ADR-039 held-event release/drop), andevent_outbox_claim_order_idx((attempts, enqueued_at) WHERE published_at IS NULL, #1201 — matches the #1093 claim orderORDER BY attempts, enqueued_atso deep-backlog recovery after a broker outage stays linear-class instead of sorting every candidate row) — and five generated migration files (20260508000000_create_event_outbox.sql,20260518004851_event_outbox_lease_columns.sql,20260713000000_event_outbox_hold.sql,20260803000000_event_outbox_v7_pk_default.sql,20260815000000_event_outbox_claim_order_idx.sql), identical in every publishing service. A 7-day janitor reaps published rows. This is the SINGLE home for the outbox schema description — per-service data-model pages point here instead of restating it (#1058). -
scheduler_runs(#1211, scale audit H2) — present in every service running a fenced daily scheduler (renewals, enrollment, applications, appeals). The wall-clock window fence behindcanopy_db::window_fence::run_daily_fenced: four columns(job_name, window_start, started_at, completed_at), PK(job_name, window_start). A background probe claims the(job, UTC-day)window withINSERT .. ON CONFLICT DO NOTHING— the single winner runs the tick, every other probe (any replica, any boot time, any restart) skips; the pre-#1211 advisory-lock-only pattern deduped only concurrent ticks, so boot-staggered replicas each ran their own daily pass. Rows are the fence, not history: a failed tick deletes its row (window retried by the next hourly probe), a successful one is stampedcompleted_at; growth is one row per job per day, no automated pruning. The schema is single-sourced incrates/canopy-db/scheduler-migrations/(one migration,20260905000000_create_scheduler_runs.sql) and parity-gated alongside the outbox family bycargo xtask outbox-migrations. This is the SINGLE home for thescheduler_runsdescription — per-service pages point here. -
_sqlx_migrations— sqlx’s own migration history table; present in every service.
Cross-service references
When a cross-service FK is shown in a per-service ERD (e.g. caps_applications.household_id → canopy-persons.households.id), it is application-level only — no Postgres FOREIGN KEY constraint exists. The orchestrator (canopy-eligibility) is responsible for supplying real IDs; the program DB cannot enforce existence in another DB.
Currency
All 14 per-service pages carry the same content shape: prose-bearing Tables list, Mermaid ER diagram with column-level types and cross-service-FK annotations, ADR-001 boundary paragraph, service-specific Retention regime, full Index list, and Migration-file inventory. canopy-caps was the worked example seed (#419); the remaining 13 were filled in by #454 against migration SQL on 2026-05-14. The migration SQL remains the source of truth — when a migration lands, the corresponding page is updated in the same MR per the doc-sweep discipline.