PSA/ee/docs/plans/2026-03-15-multiple-email-addresses-per-contact-design.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

3.6 KiB

Design — Multiple Email Addresses Per Contact

  • Date: 2026-03-15
  • Status: Approved

Summary

Adopt a hybrid contact-email model that preserves contacts.email as the required authoritative default email while adding labeled non-default email rows in a child table. This keeps the current default-email behavior intact for a large part of the application and narrows the true change surface to contact editing, cross-location uniqueness, additional-email lookup, and inbound/workflow semantics.

Decisions

  • Email addresses remain unique per tenant across both the primary email column and the additional-email table.
  • contacts.email remains required and authoritative.
  • The primary/default email gets a label.
  • Additional emails are non-default only.
  • Changing the default email is implemented as a swap:
    • selected additional email moves into contacts.email
    • previous contacts.email moves into the additional-email table
    • label metadata swaps with it
  • Canonical email labels are work, personal, billing, and other.
  • Existing default-email consumers should keep using contacts.email wherever possible.
  • Inbound sender matching must work across both primary and additional emails and preserve the matched sender separately when it differs from contacts.email.

Proposed Model

Primary/default email remains on contacts:

  • email
  • primary-email label metadata

Additional emails live in a child table:

  • contact_additional_email_address_id
  • email_address
  • normalized_email_address
  • label metadata
  • display_order

Custom labels are tenant-scoped and shared between the primary email and the additional-email rows.

UI Shape

Use a shared ContactEmailAddressesEditor with:

  • a pinned primary/default row
  • additional rows underneath
  • canonical label dropdown plus custom labels
  • promote-to-default action that performs the swap
  • no direct delete for the primary row

Detail screens show the primary/default email distinctly and list additional emails below it. List and picker surfaces continue showing contacts.email as the default summary.

What Still Changes

  1. Contact schema, migrations, validation, hydration, and swap behavior
  2. Contact create/edit/detail/import/export UI
  3. Search and lookup paths that must match additional emails
  4. Inbound email matching and workflow contact lookup behavior
  5. REST, n8n, and integration contracts that need to expose or parse additional emails

What Simplifies

  1. Most outbound email sends can keep using contacts.email
  2. Portal/auth/client-user flows can keep using contacts.email
  3. Ticket and comment summary payloads can keep using scalar email summaries
  4. Billing/survey/project notification paths mostly stay anchored on contacts.email

Risks

  • Cross-location uniqueness must be enforced reliably between the primary email column and the child table.
  • Default-swap behavior must be transactional.
  • Email-keyed snapshot recipients such as watchers should remain intentionally snapshot-based unless a later product decision says otherwise.
  • CSV/API/integration contracts still need coordinated updates even though the core default-email compatibility is preserved.

Acceptance Shape

The feature is complete when:

  • Contacts can keep a labeled primary email and optional labeled additional emails.
  • Promoting an additional email to default swaps it into contacts.email.
  • Any stored email address can resolve a contact uniquely within a tenant.
  • Existing default-email consumers continue to work from contacts.email.
  • Contracts that need the richer model can read and write primary-email labels plus additional-email rows.