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

Cross-cutting tables

  • event_outbox (ADR-018 + ADR-039) — present in every service that publishes domain events; transactional outbox drained by an in-process OutboxDrainer to 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 in crates/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. The hold_operation_id/hold_generation pair (ADR-039) lets a producer stage an event held (publish_tx_held) — skipped by the drainer until release_held (delivered) or drop_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), and event_outbox_claim_order_idx ((attempts, enqueued_at) WHERE published_at IS NULL, #1201 — matches the #1093 claim order ORDER BY attempts, enqueued_at so 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 behind canopy_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 with INSERT .. 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 stamped completed_at; growth is one row per job per day, no automated pruning. The schema is single-sourced in crates/canopy-db/scheduler-migrations/ (one migration, 20260905000000_create_scheduler_runs.sql) and parity-gated alongside the outbox family by cargo xtask outbox-migrations. This is the SINGLE home for the scheduler_runs description — 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.

Edit this page · default