---
id: CON-CRM-ENT-001
name: Contact
feature: FEAT-CRM-ENT-01
product_area: crm
module: entities
revision: 1
status: approved
product_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
definition: >-
  A contact is the continuing record of a person the customer needs to recognize, reach or connect
  to organizations.
owned_by: MOD-CRM-ENT
acted_on_by:
  - CAP-CRM-ENT-001
  - CAP-CRM-ENT-003
  - CAP-CRM-ENT-004
  - CAP-CRM-ENT-005
  - CAP-CRM-ENT-006
  - CAP-CRM-ENT-007
  - CAP-CRM-ENT-008
  - CAP-CRM-ENT-009
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-013
  - CAP-CRM-ENT-014
  - CAP-CRM-ENT-015
  - CAP-CRM-ENT-016
visible_to:
  - ACT-CRM-001
  - ACT-CRM-002
  - ACT-CRM-003
  - ACT-CRM-004
  - ACT-CRM-005
relationships:
  - to: CON-CRM-ENT-003
    statement: Affiliations connect people and organizations.
    provenance: observed
    source: 'services-crm:modules/entities/db/contact-organization-affiliations.table.ts'
  - to: CON-CRM-ENT-033
    statement: Record safeguards preserve and explain changes to the owning record.
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/record-locks.ts'
facets:
  - name: Classification and names
    meaning: 'Types classify people; name components support derived display, full and sort names.'
    used_for: 'Recognizing, presenting and finding people.'
  - name: E-mail
    meaning: Ordered addresses distinguish the primary address from alternatives.
    used_for: Reaching a person and managing primary-address uniqueness.
  - name: Phones and addresses
    meaning: Typed and preferred details include seasonal mailing choices.
    used_for: Selecting appropriate contact points for the workspace date.
  - name: Source and verification
    meaning: 'Owned, copied, linked and disconnected details distinguish origin and verification needs.'
    used_for: Sharing organizational details and resolving removed sources.
  - name: Communication choices
    meaning: Restrictions and preferences record how a person wishes to be contacted.
    used_for: Supplying choices to communication workflows.
  - name: Custom information
    meaning: Values extend a contact using appropriate Platform definitions.
    used_for: Keeping customer-specific information with the person.
  - name: Status
    meaning: >-
      Active, inactive, deceased and duplicate describe the contact standing separately from
      archive.
    used_for: 'Managing selection, automatic roles and primary-address reservation.'
  - name: Personal connections and recognition
    meaning: 'A reciprocal spouse connection, photo and number help identify the person.'
    used_for: Recognizing and distinguishing people.
evidence:
  - >-
    services-crm at 7ea48a414c42eade10d60753b8bcc323b1dd726f: docs/entities-api.md;
    docs/affiliations/README.md; docs/archive.md; docs/record-locks.md; docs/record-history.md;
    docs/disconnected-information.md; docs/milestones/CRM-M1-contacts-and-organizations.md and
    CRM-M2-affiliations-roles-compliance.md; modules/entities/db/contacts.table.ts,
    organizations.table.ts, contact-organization-affiliations.table.ts;
    services/persistence/record-locks.ts; services/affiliations/affiliation-date-reconciliation.ts;
    services/organizations/compliance/contact-role-compliance.ts.
  - >-
    react-console at da63075876b2c5f4a6af07d457bb24ea8bf4c0c7:
    components/forms/contacts/affiliation-components.tsx;
    components/contact-editor/contact-editor.tsx;
    acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0201.spec.ts; existing capability and
    journey evidence bindings in this corpus.
approved_by: Andrew
approved_at: 2026-10-22T00:00:00.000Z
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Why It Exists
    provenance: inferred
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a contact
      with neither a first nor a last name, naming the rule;
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds the full name from
      every part in order, trimmed and singly spaced;
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a full name
      sent instead of name parts, and never splits it into parts;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      an honorific salutation to the prefix, start-cased and dotted (RB-2367);
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
      every overlong name part of one request at once;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id the body named,
      which is no UUIDv7;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps
      the number consumed when the transaction rolls back: a gap, never given back;
      services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
      the retry with the contact the first attempt made;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for a type the tenant
      does not have, and saves no contact;
      services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
      a status outside the closed list by contacts_status_check;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
      the other spouse back when a contact is created naming it;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for a photo whose
      upload is not there, naming the field that failed;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for a cursor it did
      not answer; services-crm:modules/entities/services/persistence/contacts.ts:listContactFields;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#answers
      a contact whose affiliation to the organization is not its primary one;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#includes
      a contact whose affiliation in effect to an organization of the walk is not its primary one
      (M2-D3); services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only the
      contacts a picker may offer: not the inactive, not the deceased;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only the fields
      `?fields=` named, with the id;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the other tenant its own
      contact for that id, to a context that covers it;
      services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes;
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js#is refused
      where the assignment DENIES crm.application.access, though the route itself is served;
      services-crm:modules/entities/tests/e2e/platform-scope.e2e.spec.js#refuses every one of them
      to a staff slice reaching every tenant, 403 at the tenant boundary;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#CRM-ACC-0064;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0064.spec.ts#CRM-ACC-0064;
      services-crm:modules/entities/tests/e2e/display-links.e2e.spec.js#answers a presigned GET that
      lasts 15 minutes and downloads as an attachment for a document custom field;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#D-01;
      services-crm:modules/entities/services/quick-search/quick-search-indexer.ts:quickSearchIndexerOf;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a fourth e-mail address;
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#refuses
      to make an inactive contact active while another contact holds its primary address;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      two numbers of the same type;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#formats
      a valid US number, drops a blank one and marks the default preferred number;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#marks
      the first address of the tenant’s display order as preferred, never one with a seasonal
      period;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      an unknown country, naming the address’s country;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#leaves
      a contact saveable after the type of an address it holds became organizations-only;
      services-crm:modules/entities/tests/unit/services/contacts/seasonal-addresses.spec.js#refuses
      overlapping seasons naming both addresses;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the seasonal address in
      season when the contact marks it, with the workspace date;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A9;
      services-crm:modules/entities/db/contacts.table.ts:do_not_mail;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a communication type listed twice, naming the second entry;
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription value that is not shaped like an address by the domain’s
      email_address_format_check;
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription status outside the closed list by
      contact_communication_preferences_status_check;
      services-crm:modules/entities/services/persistence/contacts.ts:communicationPreferenceProblems;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#2. Scope;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 400 for an
      organization the tenant does not have;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0202.spec.ts#CRM-ACC-0202;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0177.spec.ts#CRM-ACC-0177;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      two affiliations newly marked primary at once;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a primary affiliation that starts after today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#makes
      nothing primary in its place;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#finds
      periods that share their last and first day overlapping;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      to move the affiliation to another organization under the same id (docs/affiliations/README.md
      §0b.2);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#counts
      only current and upcoming affiliations against the limit: ended history is unlimited (M2-D5);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the previous primary the day before the effective date by default, and its open roles with it;
      react-console:lib/affiliations/section.ts:affiliationBeingLeft;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#gives
      the contact the primary affiliation's job title when the save changed only the affiliation's;
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses 400 a
      second contact naming the same affiliation id, saying another affiliation has it;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 403
      permission_missing to a caller who may read organizations but not contacts;
      react-console:components/console/organization/affiliation-dialogs.tsx:ContactPrimaryInfo;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0172.spec.ts#CRM-ACC-0172
    note: 'Business need inferred from the sourced record behavior, without inventing product history.'
  - section: What It Controls
    provenance: observed
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a contact
      with neither a first nor a last name, naming the rule;
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds the full name from
      every part in order, trimmed and singly spaced;
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a full name
      sent instead of name parts, and never splits it into parts;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      an honorific salutation to the prefix, start-cased and dotted (RB-2367);
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
      every overlong name part of one request at once;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id the body named,
      which is no UUIDv7;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps
      the number consumed when the transaction rolls back: a gap, never given back;
      services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
      the retry with the contact the first attempt made;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for a type the tenant
      does not have, and saves no contact;
      services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
      a status outside the closed list by contacts_status_check;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
      the other spouse back when a contact is created naming it;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for a photo whose
      upload is not there, naming the field that failed;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for a cursor it did
      not answer; services-crm:modules/entities/services/persistence/contacts.ts:listContactFields;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#answers
      a contact whose affiliation to the organization is not its primary one;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#includes
      a contact whose affiliation in effect to an organization of the walk is not its primary one
      (M2-D3); services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only the
      contacts a picker may offer: not the inactive, not the deceased;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only the fields
      `?fields=` named, with the id;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the other tenant its own
      contact for that id, to a context that covers it;
      services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes;
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js#is refused
      where the assignment DENIES crm.application.access, though the route itself is served;
      services-crm:modules/entities/tests/e2e/platform-scope.e2e.spec.js#refuses every one of them
      to a staff slice reaching every tenant, 403 at the tenant boundary;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#CRM-ACC-0064;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0064.spec.ts#CRM-ACC-0064;
      services-crm:modules/entities/tests/e2e/display-links.e2e.spec.js#answers a presigned GET that
      lasts 15 minutes and downloads as an attachment for a document custom field;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#D-01;
      services-crm:modules/entities/services/quick-search/quick-search-indexer.ts:quickSearchIndexerOf;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a fourth e-mail address;
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#refuses
      to make an inactive contact active while another contact holds its primary address;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      two numbers of the same type;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#formats
      a valid US number, drops a blank one and marks the default preferred number;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#marks
      the first address of the tenant’s display order as preferred, never one with a seasonal
      period;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      an unknown country, naming the address’s country;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#leaves
      a contact saveable after the type of an address it holds became organizations-only;
      services-crm:modules/entities/tests/unit/services/contacts/seasonal-addresses.spec.js#refuses
      overlapping seasons naming both addresses;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the seasonal address in
      season when the contact marks it, with the workspace date;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A9;
      services-crm:modules/entities/db/contacts.table.ts:do_not_mail;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a communication type listed twice, naming the second entry;
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription value that is not shaped like an address by the domain’s
      email_address_format_check;
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription status outside the closed list by
      contact_communication_preferences_status_check;
      services-crm:modules/entities/services/persistence/contacts.ts:communicationPreferenceProblems;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#2. Scope;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 400 for an
      organization the tenant does not have;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0202.spec.ts#CRM-ACC-0202;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0177.spec.ts#CRM-ACC-0177;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      two affiliations newly marked primary at once;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a primary affiliation that starts after today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#makes
      nothing primary in its place;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#finds
      periods that share their last and first day overlapping;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      to move the affiliation to another organization under the same id (docs/affiliations/README.md
      §0b.2);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#counts
      only current and upcoming affiliations against the limit: ended history is unlimited (M2-D5);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the previous primary the day before the effective date by default, and its open roles with it;
      react-console:lib/affiliations/section.ts:affiliationBeingLeft;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#gives
      the contact the primary affiliation's job title when the save changed only the affiliation's;
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses 400 a
      second contact naming the same affiliation id, saying another affiliation has it;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 403
      permission_missing to a caller who may read organizations but not contacts;
      react-console:components/console/organization/affiliation-dialogs.tsx:ContactPrimaryInfo;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0172.spec.ts#CRM-ACC-0172
---

# Contact

## What It Is

A Contact is the continuing CRM record of a person an association needs to recognize, reach, or connect to organizations. Staff create it from name components and the other information they know. Its identity persists when the person changes a phone number, employer, title, or classification; those are changes to the person record rather than reasons to create another person.

The record has a workspace identity and an assigned contact number, with derived names for display and sorting. It holds personal information and owns collections such as contact points, communication preferences, custom values, and affiliations. A contact can exist without a classification, while a chosen Contact Type supplies a specific workspace category.

## Why It Exists

Our reading is that association work needs a shared person identity because the same member can appear in many operational situations. Treating an employer name or e-mail address as the person would make changes of circumstance look like changes of identity. A continuing contact lets staff correct the information they know and preserves a reference for organizational relationships and other applications' work.

## What It Is Not

A Contact is not a membership, a user account, or an organization. Calling a contact Member in the workspace's classification does not itself establish a membership entitlement. Platform security owns users and access, while a person record contains CRM information. A spouse reference is an interim personal connection, not a general household or person-relationship model. Archive is also separate from the person's business status.

## What It Controls

The contact owns the information saved and validated with it, including affiliations and their role periods. Changing those owned values can change related organizational coverage through follow-up processing. Its name components determine derived names used by display and discovery. Contact Type influences effective presentation assignments; current primary affiliation supplies prominent organization and title context without excluding secondary affiliations.

Status affects ordinary selection, primary-e-mail reservation, and eligibility for new automatic roles under their capability contracts. Archive and locks impose separate availability and mutation constraints. A record version anchors edits, including system changes to the person, and committed differences supply attributed history. These effects have their authoritative rule homes in the corresponding capabilities rather than in the contact definition alone.

## How It Is Used

The CRM Editor creates and corrects contacts, while the CRM Reader finds and inspects them. Create a member record and inspect the saved information follows entry through visible cards, service values, and history. Record an employer change while retaining the previous affiliation shows how a person keeps one identity while organizational context changes.

Good use distinguishes a correction from a duplicate creation and a current label from the full relationship history. Staff inspect a contact's current version before mutation and investigate source states on shared details. Integrations submit values under the same record rules; their channel does not make foreign references or stale edits acceptable.

## Example

Ruth Kim joins a professional association while employed by Harbor Engineering and serving a regional chapter. Staff create one contact with those separate affiliations and appropriate communication details. A later employer change ends the Harbor tie and adds a new one without replacing Ruth's person identity. Her chapter role remains intelligible as a separate responsibility, and staff can inspect the former employer through past affiliations.

If Ruth's work address follows her firm's main address, its source is a relationship-backed link rather than a claim that Ruth independently owns that address. Source removal can mark it disconnected and require staff attention without creating a new person. The shared identity keeps this work together.

## How It Relates

Affiliation joins the Contact to an Organization; Contact Role describes responsibilities within that tie. Contact Type classifies the person, and Contact Details and Communication Preferences explains how staff reach them and interpret instructions. Record Protection and History explains versions, locks, lifecycle actions, and revisions. Platform metadata owns custom field definitions, forms, layouts, and files; other applications own business records that reference the person.
