Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
10 KiB
PRD — Multiple Email Addresses Per Contact
- Slug:
2026-03-15-multiple-email-addresses-per-contact - Date:
2026-03-15 - Status:
Draft
Summary
Allow contacts to keep a required primary email on contacts.email, add labeled additional email addresses in a child table, and let users promote an additional email to default by swapping it into contacts.email. The design preserves compatibility for the large set of existing application surfaces that already treat contacts.email as the canonical default email while still enabling inbound matching and search across multiple stored addresses.
Problem
The current contact model allows only one email address, which is too restrictive for real customer records. A single contact may need work, personal, and billing addresses, but duplicating contacts to represent those variants causes inbound-email ambiguity and data quality issues.
At the same time, a full normalization of contact email would force contract changes across a large part of the product because many existing surfaces already read contacts.email as the default send/login/summary address. A safer design should add support for more addresses without breaking those default-email consumers.
Goals
- Let a contact store multiple labeled email addresses.
- Keep
contacts.emailas the required authoritative default email. - Let users change the default by swapping an additional email into
contacts.email. - Support canonical labels plus tenant-scoped custom labels using the same general model as contact phone labels.
- Enforce tenant-wide uniqueness across both primary and additional contact emails.
- Match inbound email and contact lookups by either the primary or an additional email address.
- Preserve compatibility for existing outbound, portal, auth, and summary consumers that already use
contacts.email.
Non-goals
- Replacing
contacts.emailwith a fully normalized email collection in this effort. - Making
contacts.emailnullable. - Allowing a contact to have no primary/default email.
- Treating additional emails as independent login aliases for portal or client-user auth.
- Rewriting every
contacts.emailconsumer to a new default-email field when existing behavior can remain unchanged. - Re-keying existing email-snapshot recipient records unless the surface explicitly re-resolves the contact from user input.
Users and Primary Flows
Primary users
- Service desk agents managing contacts
- Operators handling inbound email and ticketing
- Staff sending notifications, invoices, surveys, and portal invitations
- Automation and integration flows that create or resolve contacts by email
Flow A — Manage Contact Emails
- User opens contact create or edit.
- User edits the primary email row, including its label.
- User adds, labels, reorders, or deletes additional email rows.
- User promotes an additional email to default.
- The system swaps the promoted row into
contacts.emailand moves the previous primary email into the additional-email table.
Flow B — Match Inbound Email
- An inbound message arrives from a sender address.
- The system normalizes the sender address and searches both
contacts.emailand additional email rows. - The system resolves the owning contact uniquely.
- Ticket/comment authorship preserves the matched sender address.
- Any default outbound contact send still uses
contacts.email.
Flow C — Existing Default-Email Sends
- A workflow or product surface needs to email a contact.
- The surface reads
contacts.email. - The send path continues to work without needing a new derived default-email field.
- If the default was changed earlier, the promoted address is already present in
contacts.email.
UX / UI Notes
- Use a shared
ContactEmailAddressesEditormodeled on the phone-number editor. - The editor should render a pinned primary/default row plus a list of additional rows.
- Canonical labels:
work,personal,billing,other. - Custom labels must support freeform entry and tenant-scoped suggestions.
- Users cannot delete the primary email row directly.
- To change default, users promote an additional email, which swaps it with the current primary row.
- Detail views show the primary/default email distinctly, with additional emails listed underneath.
- List screens, pickers, and summary cards continue showing
contacts.emailas the default summary address.
Functional Requirements
Data Model and Validation
FR-01: contacts.email remains required and is the authoritative default contact email.
FR-02: The primary contact email stored on contacts gains label metadata.
FR-03: Non-default contact emails are stored as an ordered child collection separate from contacts.email.
FR-04: Canonical email labels are work, personal, billing, and other, and custom labels are tenant-scoped and reusable across primary and additional emails.
FR-05: Every normalized email address is unique per tenant across both contacts.email and additional email rows.
FR-06: Promoting an additional email to default swaps both the email value and its label metadata with the current contacts.email values.
FR-07: A contact can never lose its primary/default email; deleting the current default directly is disallowed.
FR-08: Existing contacts migrate without changing their current contacts.email values and gain safe initial primary-email label metadata.
Contact CRUD and Discovery
FR-09: Contact create/edit screens support editing the primary email label plus adding, removing, reordering, and promoting additional email rows.
FR-10: Contact detail views display the primary/default email distinctly and list all additional emails with labels.
FR-11: Contact list, picker, and summary views continue to display contacts.email as the visible default email.
FR-12: Contact search, filter, import, and export by email must be able to match both primary and additional email addresses.
Inbound Email and Workflow Lookups
FR-13: Inbound email sender matching resolves contacts by either the primary or an additional stored email address.
FR-14: Inbound and workflow contexts preserve the matched sender email separately when it differs from contacts.email.
FR-15: Shared contact lookup and create-or-find helpers search both storage locations while creating new contacts with the primary email stored in contacts.email.
FR-16: Workflow/domain-event/full-contact contracts expose primary-email label metadata and additional email rows where a full contact shape is needed, while summary email fields continue to use contacts.email.
Compatibility and Integrations
FR-17: Existing outbound notifications, portal/auth flows, client-user linking, billing sends, and similar default-email consumers continue to use contacts.email without needing a new default-email field.
FR-18: Existing email-keyed watcher and recipient snapshot behavior remains compatible unless a surface explicitly re-resolves a contact from user-supplied email input.
FR-19: REST, n8n, CSV, and integration contracts add support for primary-email labels and additional email rows without removing scalar email.
FR-20: Automated coverage includes DB-backed uniqueness, swap, and lookup tests plus compatibility regressions for major contacts.email consumers.
Non-functional Requirements
- Keep the email-label editor behavior aligned with the existing phone-number label model where practical.
- Favor compatibility by preserving
contacts.emailas the authoritative default field. - Avoid ambiguous contact resolution by enforcing tenant-wide uniqueness across both storage locations.
- Keep the migration safe and reversible by preserving existing primary email values and layering additional-email support around them.
Data / API / Integrations
Proposed Shape
Primary/default email remains on contacts:
email- primary email label metadata fields
Additional emails live in a child table:
contact_additional_email_address_idemail_addressnormalized_email_address- label metadata
display_order
API Direction
- Keep
emailin create/update/read contracts as the primary/default email. - Add primary-email label fields to create/update/read contracts.
- Add
additional_email_addressesto create/update/read contracts. - Keep response summary email fields sourced from
contacts.email.
Workflow Direction
findContactByEmailand related lookup helpers must search bothcontacts.emailand additional email rows.- Workflow contexts that care about inbound authorship should preserve:
- matched sender email
- primary/default contact email from
contacts.email
Risks and Migration Notes
- The main schema risk is cross-location uniqueness: the system must prevent duplicates between
contacts.emailand the additional-email table in both directions. - Default-swap behavior must be transactional so the primary and additional rows cannot drift.
- CSV/import/export and API contracts still need real updates, even with the safer compatibility model.
- Some recipient stores, such as watcher-like email snapshots, may intentionally remain email-keyed rather than dynamically following future contact default swaps.
- Existing tests and fixtures that only know about scalar email will still need widespread updates to cover primary labels and additional emails.
Acceptance Criteria / Definition Of Done
- Contacts can keep a labeled primary/default email and add labeled additional emails.
- Users can promote an additional email to default, and the system swaps it into
contacts.email. - Tenant-wide uniqueness is enforced across both primary and additional contact emails.
- Inbound email processing and lookup helpers can resolve a contact by any stored email address.
- Existing default-email consumers continue to operate against
contacts.email. - REST/API, CSV, n8n, and integration contracts support the hybrid model.
- Automated coverage proves swap behavior, uniqueness, inbound lookup, and compatibility for key
contacts.emailconsumers.