Circulator architecture v2
Overview
Field circulators are stored in public.circulators. Per-campaign, per-team participation is tracked in public.campaign_team_circulators with a derived volunteer | paid type.
Payroll / transactions continue to reference public.users via team_users.role = circulator (see TEAM_ROLES.CIRCULATOR in App/src/config).
Sources
| Source | Description | Dedup key |
|---|---|---|
| 1 | App user with team circulator role | circulators.user_id (auto-created by DB trigger on team_users) |
| 2 | Added during signature flow (ES match or manual) | Normalized name + street + zip (addr-... unique_id) |
Type derivation
paid:circulators.user_id IS NOT NULLvolunteer: otherwise (default for signature-flow-created rows)
No UI toggle; signature flow never updates an existing (campaign, team, circulator) assignment row.
Signing lookup order
- Global
circulatorslookup by name + address - Elasticsearch voter file (
/api/essearch) - Manual entry
Key tables
public.circulators— canonical person record (user_idoptional, unique when set)public.campaign_team_circulators— PK(campaign, team, circulator), columntypepublic.campaign_circulators— legacy stats counter (still maintained by petition triggers)public.recent_circulators— view overcampaign_team_circulators(top 4 per user per campaign)
Migrations
20260519120000_circulator_architecture_refactor.sql20260519120100_team_users_circulator_role_sync.sql20260519120200_recent_circulators_and_households_mv.sql