Skip to main content

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​

SourceDescriptionDedup key
1App user with team circulator rolecirculators.user_id (auto-created by DB trigger on team_users)
2Added during signature flow (ES match or manual)Normalized name + street + zip (addr-... unique_id)

Type derivation​

  • paid: circulators.user_id IS NOT NULL
  • volunteer: otherwise (default for signature-flow-created rows)

No UI toggle; signature flow never updates an existing (campaign, team, circulator) assignment row.

Signing lookup order​

  1. Global circulators lookup by name + address
  2. Elasticsearch voter file (/api/essearch)
  3. Manual entry

Key tables​

  • public.circulators — canonical person record (user_id optional, unique when set)
  • public.campaign_team_circulators — PK (campaign, team, circulator), column type
  • public.campaign_circulators — legacy stats counter (still maintained by petition triggers)
  • public.recent_circulators — view over campaign_team_circulators (top 4 per user per campaign)

Migrations​

  • 20260519120000_circulator_architecture_refactor.sql
  • 20260519120100_team_users_circulator_role_sync.sql
  • 20260519120200_recent_circulators_and_households_mv.sql