---
id: DOM-CRM-ENT-003
title: Affiliation
realises: CON-CRM-ENT-003
product_area: crm
module: entities
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
owned_by: SVC-CRM-ENT-entities
lifecycle: STATE-CRM-ENT-005
custom_fields: none
identity: >-
  Each affiliation has a workspace-unique identity, accepting a valid lowercase
  time-ordered client value on creation. An idless entry matching an existing
  organization, type and start date reuses that affiliation. Role periods have
  separate identities scoped to their affiliation.
attributes:
  - name: id
    kind: text
    required: true
    computed: false
    notes: >-
      Creation accepts an unused identity appearing only once in the submitted
      list.
    set_by: staff
    rules: >-
      Creation accepts an unused identity appearing only once in the submitted
      list.
    example: 019a0f10-0000-7000-8000-000000000001
  - name: contact
    kind: reference
    required: true
    computed: false
    notes: The contact owns the affiliation permanently.
    set_by: staff
    rules: The contact owns the affiliation permanently.
    example: Ruth Kim
  - name: organization
    kind: reference
    required: true
    computed: false
    notes: >-
      The destination is a workspace organization and cannot change after
      creation.
    set_by: staff
    rules: >-
      The destination is a workspace organization and cannot change after
      creation.
    example: Harbor Engineering
  - name: affiliation type
    kind: reference
    required: true
    computed: false
    notes: >-
      The affiliation classification is workspace-local. The Organization
      command uses the configured default.
    set_by: staff
    rules: >-
      The affiliation classification is workspace-local. The Organization
      command uses the configured default.
    example: Employer
  - name: primary
    kind: enum
    required: true
    computed: false
    notes: >-
      One affiliation at most may be primary. It must have started and have no
      end date.
    set_by: staff
    rules: >-
      One affiliation at most may be primary. It must have started and have no
      end date.
    example: true
  - name: job title
    kind: text
    required: false
    computed: false
    notes: >-
      The primary affiliation and contact share this title. Whitespace-only
      content is absent.
    set_by: staff
    rules: >-
      The primary affiliation and contact share this title. Whitespace-only
      content is absent.
    example: Executive Director
  - name: start date
    kind: date
    required: false
    computed: false
    set_by: staff
    rules: Optional ISO calendar date; absence is unbounded in the past.
    example: '2026-10-01'
    notes: The first eligible day of the relationship.
  - name: end date
    kind: date
    required: false
    computed: false
    set_by: staff
    rules: >-
      Optional inclusive ISO calendar date; cannot precede start; primary is
      open-ended.
    example: '2026-10-31'
    notes: The last eligible day of the relationship.
  - name: effective state
    kind: computed
    required: true
    computed: true
    notes: >-
      The workspace date determines whether the relationship is future,
      effective or ended.
    set_by: system
    rules: >-
      The workspace date determines whether the relationship is future,
      effective or ended.
    example: current
  - name: notes
    kind: text
    required: false
    computed: false
    notes: Whitespace-only content is normalized to absence.
    set_by: staff
    rules: Whitespace-only content is normalized to absence.
    example: Represents the member firm.
  - name: role periods
    kind: collection
    required: false
    computed: false
    notes: >-
      An affiliation can contain at most 25 catalogue role periods, each dated
      within the enclosing relationship.
    set_by: staff
    rules: >-
      An affiliation can contain at most 25 catalogue role periods, each dated
      within the enclosing relationship.
    example: Billing Contact from 2026-10-01
  - name: automatic roles due
    kind: computed
    required: true
    computed: true
    notes: >-
      This pending marker schedules automatic-role assignment once a future
      relationship becomes due.
    set_by: system
    rules: >-
      This pending marker schedules automatic-role assignment once a future
      relationship becomes due.
    example: true
  - name: pending default links
    kind: computed
    required: false
    computed: true
    notes: >-
      The saved default-link choice waits until a future affiliation becomes
      effective.
    set_by: system
    rules: >-
      The saved default-link choice waits until a future affiliation becomes
      effective.
    example: Work address when effective
  - name: default link choice
    kind: collection
    required: false
    computed: false
    notes: >-
      Creation-only selection: omission chooses all type mappings, while an
      empty array chooses none.
    set_by: staff
    rules: >-
      Creation-only selection: omission chooses all type mappings, while an
      empty array chooses none.
    example: Work address selected
  - name: archived organization information
    kind: computed
    required: false
    computed: true
    notes: >-
      Organization label and archive timestamp are added when reading the
      relationship.
    set_by: system
    rules: >-
      Organization label and archive timestamp are added when reading the
      relationship.
    example: 'Harbor Engineering, archived on 2026-10-23'
  - name: type · default mappings
    kind: collection
    required: false
    computed: false
    notes: >-
      A type defines at most 20 source-to-destination address and phone
      mappings.
    set_by: staff
    rules: >-
      A type defines at most 20 source-to-destination address and phone
      mappings.
    example: Main address to Work address
  - name: role catalogue · settings
    kind: collection
    required: false
    computed: false
    notes: >-
      The role catalogue supplies name, description, availability, order,
      locking and automatic or first-contact settings.
    set_by: staff
    rules: >-
      The role catalogue supplies name, description, availability, order,
      locking and automatic or first-contact settings.
    example: 'Billing Contact, active, automatic'
  - name: role period identity
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: >-
      Unique within the nested save; an existing identity remains in its owning
      affiliation.
    example: 019a0f10-0000-7000-8000-000000000002
    notes: The service assigns new role identities; later edits submit the saved identity of the same responsibility episode.
  - name: role definition
    kind: reference
    required: false
    computed: false
    set_by: staff
    rules: >-
      Workspace contact role; new assignments obey organization-type
      restrictions.
    example: Billing Contact
    notes: The workspace responsibility held in this episode.
  - name: role start date
    kind: date
    required: false
    computed: false
    set_by: staff
    rules: >-
      Optional date bounded by the parent affiliation; omission follows its
      bound.
    example: '2026-10-01'
    notes: The first day of this responsibility.
  - name: role end date
    kind: date
    required: false
    computed: false
    set_by: staff
    rules: Inclusive date within the affiliation; same-role periods cannot overlap.
    example: '2026-10-31'
    notes: The final day of this responsibility.
  - name: element lock
    kind: enum
    required: false
    computed: false
    set_by: staff
    rules: Aggregate safeguard checks apply to protected nested elements.
    example: deletes
    notes: The customer restriction on the relationship or responsibility.
  - name: Role catalogue / id
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Stable workspace role identity used by periods and policy.
    example: billing-contact
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / name
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Staff-facing responsibility label.
    example: Billing Contact
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / description
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Optional explanation of the responsibility.
    example: Receives dues correspondence
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / is_active
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: 'Availability of the definition, separate from a saved period’s dates.'
    example: 'true'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / display_order
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: >-
      Optional administrator order, with alphabetical fallback for unordered
      definitions.
    example: '2'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / administrative_contact
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: >-
      Saved administrative-contact classification; the built role editor does
      not expose a switch for this property.
    example: 'true'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / assign_automatically
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Requests automatic assignment for people joining an organization.
    example: 'true'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Role catalogue / assign_automatically_to_first_contact
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: >-
      Requests assignment for the first eligible person; evaluation remains
      deterministic system work.
    example: 'false'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Affiliation type / id
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Stable classification identity for a relationship.
    example: employee
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Affiliation type / name
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Staff-facing classification name.
    example: Employee
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Affiliation type / description
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Optional explanation of this kind of tie.
    example: Works for the association member
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Affiliation type / is_active
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: 'Availability for new selection, without invalidating retained references.'
    example: 'true'
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Information default / kind
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Address or phone mapping kind.
    example: address
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Information default / source type
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Organization purpose supplying the default.
    example: headquarters
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - name: Information default / destination type
    kind: text
    required: false
    computed: false
    set_by: staff
    rules: Compatible contact purpose receiving the link.
    example: work
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
relationships:
  - to: DOM-CRM-ENT-001
    cardinality: many-to-one
    ownership: The contact owns the affiliation and is its only save boundary.
    cross_service: false
    on_archive_or_delete: >-
      Contact archive retains ties; contact deletion cascades this affiliation
      and its roles.
  - to: DOM-CRM-ENT-002
    cardinality: many-to-one
    ownership: The affiliation owns its reference to the organization.
    cross_service: false
    on_archive_or_delete: >-
      Organization archive retains labels; organization deletion is blocked
      while this affiliation exists.
  - to: DOM-CRM-ENT-033
    cardinality: the contact's safeguards cover its affiliations
    ownership: >-
      Protection and revisions apply through the owning contact rather than a
      separate affiliation version.
    cross_service: false
    on_archive_or_delete: >-
      Removing the affiliation is captured through contact history; contact
      deletion retains historical revisions.
invariants:
  - id: INV-DOM-CRM-ENT-003-01
    statement: >-
      Organization and affiliation-type references must resolve in the
      workspace; failure aborts the enclosing contact save.
    enforced_at:
      - api
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for an organization the tenant does not have
    protects: >-
      A tie between real local identities rather than a cross-workspace
      relationship.
  - id: INV-DOM-CRM-ENT-003-02
    statement: >-
      A contact can designate only one primary affiliation, with a start date no
      later than today and no end date.
    enforced_at:
      - service
      - database
      - console
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      two affiliations newly marked primary at once
    protects: One prominent organization label that reflects an eligible relationship.
  - id: INV-DOM-CRM-ENT-003-03
    statement: >-
      For one contact, periods to the same organization with the same
      affiliation type cannot overlap, including shared boundary days and
      unbounded ends.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#finds
      periods that share their last and first day overlapping
    protects: >-
      Distinct episodes of one kind of relationship without double-counted
      dates.
  - id: INV-DOM-CRM-ENT-003-04
    statement: The relationship end date must not precede its start.
    enforced_at:
      - service
      - database
      - console
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts
    protects: An interpretable inclusive relationship period.
  - id: INV-DOM-CRM-ENT-003-05
    statement: An existing affiliation cannot be reassigned to another organization.
    enforced_at:
      - service
    provenance: test
    source: >-
      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)
    protects: Stable history of which organization a relationship concerns.
  - id: INV-DOM-CRM-ENT-003-06
    statement: >-
      The limit of 100 affiliations counts current and future relationships and
      excludes ended ones.
    enforced_at:
      - service
    provenance: test
    source: >-
      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)
    protects: Bounded current work without discarding ended history.
  - id: INV-DOM-CRM-ENT-003-07
    statement: >-
      Role periods must fit inside the affiliation dates. Periods for the same
      role cannot overlap, and the list is limited to 25 entries.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      the same role overlapping itself on one affiliation
    protects: >-
      Role coverage that never extends outside its parent tie or double-counts
      one period.
  - id: INV-DOM-CRM-ENT-003-08
    statement: >-
      Assigned roles must belong to the workspace, even when the catalogue role
      is inactive.
    enforced_at:
      - service
      - database
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contact-affiliations.ts:affiliationRefusal
    protects: Dated responsibilities backed by the workspace vocabulary.
  - id: INV-DOM-CRM-ENT-003-09
    statement: >-
      Saving preserves equality between the contact job title and primary
      affiliation title.
    enforced_at:
      - service
    provenance: test
    source: >-
      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
    protects: Agreement between the person’s prominent title and organizational context.
  - id: INV-DOM-CRM-ENT-003-10
    statement: >-
      Automatic assignment waits until the affiliation is due and does not
      duplicate an existing role or reassign a role after its pending assignment
      was settled.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#does
      not give back a role a person removed after it was given
    protects: User corrections that repeated automatic processing does not undo.
  - id: INV-DOM-CRM-ENT-003-11
    statement: >-
      Competing joins to an organization with no effective affiliates serialize
      first-contact assignment so only one contact receives those roles.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the first-contact roles to exactly one of several contacts joining an
      empty organization at once
    protects: A single first-contact decision under simultaneous joins.
  - id: INV-DOM-CRM-ENT-003-12
    statement: >-
      Removal requires resolving every contact detail still linked through the
      affiliation in the submitted save.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a save that removes the affiliation and keeps the rows linked through it
    protects: Source links that do not outlive a removed authorization relationship.
  - id: INV-DOM-CRM-ENT-003-13
    statement: >-
      Deletion of an affiliation type is blocked by relationship references and
      by its use as the workspace default.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      the delete (409) while an affiliation of a contact is of that type
    protects: Existing affiliations and default commands backed by a surviving type.
  - id: INV-DOM-CRM-ENT-003-14
    statement: >-
      A contact role cannot be deleted while a role period or organization-type
      compliance rule references it.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js#refuses
      it with the legacy words, naming the organization types whose rules depend
      on it
    protects: Responsibility history and policy references backed by a surviving role.
  - id: INV-DOM-CRM-ENT-003-15
    statement: >-
      Affiliations and their type and role catalogues do not declare
      custom-field storage.
    enforced_at:
      - api
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#refuses
      a create that carries custom_fields, as an unknown property
    protects: >-
      The intended extension boundary, with custom values remaining on people
      and organizations.
physical:
  - contact_organization_affiliations
  - contact_organization_roles
  - affiliation_types
  - affiliation_type_contact_info_defaults
  - contact_roles
  - contact_organization_affiliations_current
  - contact_organization_affiliations_for_queries
evidence:
  - >-
    services-crm at 7ea48a41:
    modules/entities/db/contact-organization-affiliations.table.ts,
    contact-organization-roles.table.ts, affiliation-types.table.ts,
    affiliation-type-contact-info-defaults.table.ts, contact-roles.table.ts;
    services/persistence/contact-affiliations.ts, affiliation-types.ts,
    contact-roles.ts; services/contacts/role-assignment/*.ts;
    services/affiliations/*.ts; docs/affiliations/README.md.
  - >-
    services-platform at ad3f6a65: docs/standards/custom-field-capability.md §2
    (affiliations and contact roles have none).
permissions:
  - action: read
    permission: crm.contacts.read
    roles: [Administrator, Power User, Standard User, Read Only]
  - action: update
    permission: crm.contacts.update
    roles: [Administrator, Power User, Standard User]
    by_state: >-
      Ordinary update refuses archived or fully locked records; archive actions
      also check full locks.
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: What It Models
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
  - section: Identity and Attributes
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
  - section: Relationships
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
  - section: Lifecycle
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
  - section: How It Is Stored and Shared
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
  - section: Permissions
    provenance: documented
    source: 'rhythm-v2-docs:specs/crm/AUTHORIZATION.md#Permission Catalogue'
  - section: Invariants
    provenance: inferred
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
    note: >-
      Protection explanations interpret the existing invariant rows and enforcement boundaries; no
      new product decision is claimed.
  - section: What It Models
    provenance: inferred
    source: 'services-crm:modules/entities/services/persistence/contact-affiliations.ts'
    note: The explicitly labeled aggregate rationale follows the object boundary and its owned records.
  - section: Open Design Questions
    provenance: documented
    source: 'rhythm-v2-docs:specs/crm/entities/FINDINGS.md#Findings'
  - section: Identity and Attributes
    provenance: observed
    source: >-
      services-crm:modules/entities/db/contact-roles.table.ts;
      services-crm:modules/entities/db/affiliation-types.table.ts
  - section: Invariants
    provenance: inferred
    source: "services-crm:modules/entities/services/persistence/contacts.ts; services-crm:modules/entities/services/persistence/organizations.ts; services-crm:modules/entities/services/organizations/compliance/organization-compliance.ts; services-crm:modules/entities/services/persistence/record-locks.ts"
    note: "The rationale interprets this artifact’s sourced invariants and linked capability rules; it does not claim a recorded product decision."
---

# Affiliation

## What It Models

An affiliation models one episode of a person’s connection to an organization. Ruth can work for Harbor Engineering and serve Harbor West Chapter at the same time, with distinct responsibilities and dates on each tie. The contact owns each affiliation and its nested role periods even when staff edit from the organization page.

Our reading is that an episode needs its own identity so an employer change can retain the prior stay without changing which body that earlier connection describes. Affiliation Type supplies the relationship meaning and sharing defaults; Contact Role supplies the vocabulary for responsibilities held during the episode.

## Identity and Attributes

The values below distinguish identities returned by a read from creation inputs. Ruth’s integration can choose a valid, unused affiliation identity before submitting the contact. A new Billing Contact role period instead omits its identity and receives one from the service. Returning that saved role identity later identifies the existing period; supplying an invented role identity does not create another responsibility.

Each affiliation has a workspace-unique identity, accepting a valid lowercase time-ordered client value on creation. An idless entry matching an existing organization, type and start date reuses that affiliation. Role periods have separate identities scoped to their affiliation.

The table describes logical properties. Optional values can be absent; a required collection member is distinct from requiring the collection itself. Examples illustrate association information and do not establish new defaults. Service-owned values remain separate from fields that staff or integrations supply.

| Property | Meaning and writer | Constraints | Example |
|---|---|---|---|
| id | Staff can supply an unused lowercase UUIDv7 for a new affiliation; an existing affiliation retains its saved identity. | Creation accepts an unused identity appearing only once in the submitted list. | 019a0f10-0000-7000-8000-000000000001 |
| contact | The contact owns the affiliation permanently. Set by staff. | The contact owns the affiliation permanently. | Ruth Kim |
| organization | The destination is a workspace organization and cannot change after creation. Set by staff. | The destination is a workspace organization and cannot change after creation. | Harbor Engineering |
| affiliation type | The affiliation classification is workspace-local. The Organization command uses the configured default. Set by staff. | The affiliation classification is workspace-local. The Organization command uses the configured default. | Employer |
| primary | One affiliation at most may be primary. It must have started and have no end date. Set by staff. | One affiliation at most may be primary. It must have started and have no end date. | true |
| job title | The primary affiliation and contact share this title. Whitespace-only content is absent. Set by staff. | The primary affiliation and contact share this title. Whitespace-only content is absent. | Executive Director |
| start date | The first eligible day of the relationship. Set by staff. | Optional ISO calendar date; absence is unbounded in the past. | 2026-10-01 |
| end date | The last eligible day of the relationship. Set by staff. | Optional inclusive ISO calendar date; cannot precede start; primary is open-ended. | 2026-10-31 |
| effective state | The workspace date determines whether the relationship is future, effective or ended. Set by system. | The workspace date determines whether the relationship is future, effective or ended. | current |
| notes | Whitespace-only content is normalized to absence. Set by staff. | Whitespace-only content is normalized to absence. | Represents the member firm. |
| role periods | An affiliation can contain at most 25 catalogue role periods, each dated within the enclosing relationship. Set by staff. | An affiliation can contain at most 25 catalogue role periods, each dated within the enclosing relationship. | Billing Contact from 2026-10-01 |
| automatic roles due | This pending marker schedules automatic-role assignment once a future relationship becomes due. Set by system. | This pending marker schedules automatic-role assignment once a future relationship becomes due. | true |
| pending default links | The saved default-link choice waits until a future affiliation becomes effective. Set by system. | The saved default-link choice waits until a future affiliation becomes effective. | Work address when effective |
| default link choice | Creation-only selection: omission chooses all type mappings, while an empty array chooses none. Set by staff. | Creation-only selection: omission chooses all type mappings, while an empty array chooses none. | Work address selected |
| archived organization information | Organization label and archive timestamp are added when reading the relationship. Set by system. | Organization label and archive timestamp are added when reading the relationship. | Harbor Engineering, archived on 2026-10-23 |
| type · default mappings | A type defines at most 20 source-to-destination address and phone mappings. Set by staff. | A type defines at most 20 source-to-destination address and phone mappings. | Main address to Work address |
| role catalogue · settings | The role catalogue supplies name, description, availability, order, locking and automatic or first-contact settings. Set by staff. | The role catalogue supplies name, description, availability, order, locking and automatic or first-contact settings. | Billing Contact, active, automatic |
| role period identity | The service assigns the identity when it creates a responsibility episode; later edits return that saved identity. | Unique within the nested save; an existing identity remains in its owning affiliation. | 019a0f10-0000-7000-8000-000000000002 |
| role definition | The workspace responsibility held in this episode. Set by staff. | Workspace contact role; new assignments obey organization-type restrictions. | Billing Contact |
| role start date | The first day of this responsibility. Set by staff. | Optional date bounded by the parent affiliation; omission follows its bound. | 2026-10-01 |
| role end date | The final day of this responsibility. Set by staff. | Inclusive date within the affiliation; same-role periods cannot overlap. | 2026-10-31 |
| element lock | The customer restriction on the relationship or responsibility. Set by staff. | Aggregate safeguard checks apply to protected nested elements. | deletes |

### Policy and audit properties

These properties belong to the catalogue or safeguard facet named in the property column. They explain the implementation’s exposed policy and audit context without allocating another domain object. System attribution is read-only; catalogue policy is administered through its own permitted operation and is not changed by an ordinary record edit.

| Property | Meaning | Example |
|---|---|---|
| Role catalogue / id | Stable workspace role identity used by periods and policy. | billing-contact |
| Role catalogue / name | Staff-facing responsibility label. | Billing Contact |
| Role catalogue / description | Optional explanation of the responsibility. | Receives dues correspondence |
| Role catalogue / is_active | Availability of the definition, separate from a saved period’s dates. | true |
| Role catalogue / display_order | Optional administrator order, with alphabetical fallback for unordered definitions. | 2 |
| Role catalogue / administrative_contact | Saved administrative-contact classification; the built role editor does not expose a switch for this property. | true |
| Role catalogue / assign_automatically | Requests automatic assignment for people joining an organization. | true |
| Role catalogue / assign_automatically_to_first_contact | Requests assignment for the first eligible person; evaluation remains deterministic system work. | false |
| Affiliation type / id | Stable classification identity for a relationship. | employee |
| Affiliation type / name | Staff-facing classification name. | Employee |
| Affiliation type / description | Optional explanation of this kind of tie. | Works for the association member |
| Affiliation type / is_active | Availability for new selection, without invalidating retained references. | true |
| Information default / kind | Address or phone mapping kind. | address |
| Information default / source type | Organization purpose supplying the default. | headquarters |
| Information default / destination type | Compatible contact purpose receiving the link. | work |

## Relationships

Many affiliations belong to one contact, and many can name one organization. The contact is the write owner, so editing a tie requires contact update authority and the contact’s read version. The organization reference remains fixed for an existing affiliation; a later employer is a different connection.

Archiving either record preserves the tie and historical context. Deleting the contact cascades affiliation and role rows; organization deletion is blocked while the tie exists. Removing a tie through an explicit contact list also requires settling any contact detail still linked through it. Locks and revisions operate through the owning contact rather than an independent relationship edit version.

## Lifecycle

The affiliation lifecycle derives upcoming, current and past from inclusive calendar dates in the workspace timezone. Primary is an additional designation available to an effective open-ended tie. Passing a boundary changes read interpretation without requiring a midnight write to the relationship itself.

Pending automatic roles and default links are separate work. An effective affiliation can still await that work because of locks, bounded passes or a disabled reconciliation path. The lifecycle model distinguishes those effects from the calendar classification, and ending the primary tie does not silently nominate another one.

## Invariants

The affiliation preserves the identity of one tie between a person and an organization. Reusing an existing role-period identity means editing that episode; inventing an identity cannot move another affiliation’s responsibility into this tie. The organization reference remains fixed after creation, so changing employers creates distinct relationship context rather than rewriting an old employer into a new one.

The primary designation coordinates prominent organizational context with the person’s title. Effective dates and the open-ended primary requirement prevent a future or ended relationship from being presented as the person’s current primary employer. A secondary effective tie can still contribute people to organization rosters and compliance. Primary prominence is therefore separate from whether the relationship counts today.

Role policy and date validation keep responsibility periods interpretable. Automatic assignment uses saved pending state and eligible definitions; it does not turn a role label into a security grant. Contact ownership and version comparison make these nested changes part of one guarded save. Staff working from an organization page still change the person’s relationship aggregate, and organization update permission does not substitute for authority to edit the contact.

## Permissions

The CRM authorization model separates workspace access from permission for a particular action.

The service checks these grants at the corresponding operation boundary. Additional record guards still apply: grants do not bypass archive, customer restrictions, dependencies, version comparison or Rhythm protection. An organization-page relationship action changes the contact, so organization update authority alone is insufficient.

## How It Is Stored and Shared

Affiliation and role-period rows remain part of contact persistence. Current and query views expose effective organizational membership without reducing it to the primary label. The primary tie supplies the contact’s organizational subtitle and coordinates its title; secondary effective ties still contribute to applicable people discovery and role compliance.

Type and role catalogues are maintained through the configuration service hosted by CRM. They do not acquire custom-field storage simply because contact and organization records have it. Relationship changes capture a contact event, while organization compliance is normally reevaluated after commit.

## Open Design Questions

The observed aggregate-save route for ending and removal still differs from the explicit-operation requirement recorded by Platform. Save-time role eligibility, scheduled assignment and the default-link backlog also have existing findings. This design preserves their evidence and does not turn the present implementation into an approved replacement contract.
