---
id: CON-CRM-ENT-033
name: Record Protection and History
feature: FEAT-CRM-ENT-07
product_area: crm
module: entities
revision: 1
status: approved
product_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
definition: >-
  Record protection and history are the shared safeguards that preserve identity, control changes
  and explain the lifecycle of contacts and organizations.
owned_by: MOD-CRM-ENT
acted_on_by:
  - CAP-CRM-ENT-004
  - CAP-CRM-ENT-013
  - CAP-CRM-ENT-014
  - CAP-CRM-ENT-015
  - CAP-CRM-ENT-016
  - CAP-CRM-ENT-019
visible_to:
  - ACT-CRM-001
  - ACT-CRM-002
  - ACT-CRM-003
  - ACT-CRM-004
  - ACT-CRM-005
relationships:
  - to: CON-CRM-ENT-001
    statement: Shared safeguards apply to contact records.
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/record-locks.ts'
  - to: CON-CRM-ENT-002
    statement: Shared safeguards apply to organization records.
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/record-locks.ts'
facets:
  - name: Identity and numbers
    meaning: Permanent identity is distinct from staff-facing numbers.
    used_for: Referencing a record as its name and details change.
  - name: Versions and stale saves
    meaning: Versions identify the state read and prevent silent overwrites.
    used_for: Reconciling concurrent human and system edits.
  - name: Locks and release
    meaning: Record and element locks limit updates or deletion and have accountable release.
    used_for: Deliberately protecting information.
  - name: Protected system records
    meaning: Rhythm-owned protection is distinct from reversible customer locks.
    used_for: Preserving system records from customer removal or lock changes.
  - name: Revisions and attribution
    meaning: Read-only revisions explain changes and who or what made them.
    used_for: Inspecting past corrections and lifecycle actions.
  - name: Archive and deletion
    meaning: Archive retains a record; deletion removes owned data while retaining revisions.
    used_for: Choosing preservation or removal according to dependencies.
  - name: Presentation settings
    meaning: CRM assigns defaults and overrides for Platform definitions and the default affiliation type.
    used_for: 'Selecting appropriate record forms, layouts and relationship defaults.'
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.e2e.spec.js#answers 400 naming sys_version to
      a patch that tests no version;
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#writes
      no revision; services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#clears a field a
      later patch removes;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#empties
      the collection;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      the second address into the type the first left, and the preferred flag with it;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      a replacement whose contact number differs from the stored one, naming number;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#rolls
      back the contact row the transaction had already updated;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the create as ObjectInvalid, listing both problems;
      services-crm:modules/entities/tests/unit/api/contact-api-mapper.spec.js#drops a blank e-mail
      element rather than refusing the null the string standard made of it;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 naming the system,
      not a colleague, when the system cleared the spouse link while the contact was edited (M1-D6);
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0009.spec.ts#CRM-ACC-0009;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A14;
      services-crm:docs/entities-api.md#Not yet under the complete-payload rule;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a caller who may update
      contacts but not archive them, 403;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses an archive with no
      version, 400, naming it;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#keeps
      the status it had: archive is not a status;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the trimmed reason as unarchive_reason;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a replacement, 409
      RecordArchived; services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#lists it for
      archived=only, and only it; services-crm:docs/archive.md#Lists and pickers;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the ended affiliation naming the organization's id, with its name and its archive instant;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#is
      kept through a plain save, which never writes it;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0046.spec.ts#CRM-ACC-0046;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#removes
      every owned element with it, the foreign keys cascading;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#bumps
      the version of that contact and stamps the system, not the caller that deleted (M1-D6);
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked;
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers 409 to a delete
      while the contact is affiliated with it;
      services-crm:modules/entities/tests/integration/services/assets/retire-entity-assets.integration.spec.js#detaches
      the photo in the delete's own transaction;
      services-crm:modules/entities/tests/unit/services/persistence/endpoint-deletions.spec.js#never
      runs a statement when it is off;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#CRM-Q10;
      services-crm:modules/entities/aws/api/record-locks.routes.ts:defineRecordLockRoutes;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
      the role at its version and lock, and records nothing;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the page ended;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403 naming sys_locked for
      a replacement that sets a lock (E-14);
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 IllegalOperation, not
      403, for a replacement of a contact locked against updates, the lock sent back unchanged;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 to a replacement that
      only clears an element lock without the unlock key, and 200 with it (CRM-ACC-0035);
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      the unlock, naming the protection; services-crm:docs/record-locks.md#Not covered yet;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
      exactly one revision, whatever the save touched;
      services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js#answers a caller
      with the history key but no read of organizations 404, as the organization itself would;
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#refuses a query parameter
      it does not know, rather than ignoring the filter;
      services-crm:modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js#still
      answers for a user who has been deleted, because former users are named too;
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#shows the role removed
      and the role added, each under the name it has now;
      services-crm:modules/entities/aws/api/contacts.routes.ts:absorbedContactHistoryRoutes;
      services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js#may
      not insert a revision, so nothing the application runs can forge one;
      services-crm:docs/record-history.md#Fail closed, and the maintenance helper;
      services-crm:docs/record-history.md#Retention
    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.e2e.spec.js#answers 400 naming sys_version to
      a patch that tests no version;
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#writes
      no revision; services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#clears a field a
      later patch removes;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#empties
      the collection;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      the second address into the type the first left, and the preferred flag with it;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      a replacement whose contact number differs from the stored one, naming number;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#rolls
      back the contact row the transaction had already updated;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the create as ObjectInvalid, listing both problems;
      services-crm:modules/entities/tests/unit/api/contact-api-mapper.spec.js#drops a blank e-mail
      element rather than refusing the null the string standard made of it;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 naming the system,
      not a colleague, when the system cleared the spouse link while the contact was edited (M1-D6);
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0009.spec.ts#CRM-ACC-0009;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A14;
      services-crm:docs/entities-api.md#Not yet under the complete-payload rule;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a caller who may update
      contacts but not archive them, 403;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses an archive with no
      version, 400, naming it;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#keeps
      the status it had: archive is not a status;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the trimmed reason as unarchive_reason;
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a replacement, 409
      RecordArchived; services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#lists it for
      archived=only, and only it; services-crm:docs/archive.md#Lists and pickers;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the ended affiliation naming the organization's id, with its name and its archive instant;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#is
      kept through a plain save, which never writes it;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0046.spec.ts#CRM-ACC-0046;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#removes
      every owned element with it, the foreign keys cascading;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#bumps
      the version of that contact and stamps the system, not the caller that deleted (M1-D6);
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked;
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers 409 to a delete
      while the contact is affiliated with it;
      services-crm:modules/entities/tests/integration/services/assets/retire-entity-assets.integration.spec.js#detaches
      the photo in the delete's own transaction;
      services-crm:modules/entities/tests/unit/services/persistence/endpoint-deletions.spec.js#never
      runs a statement when it is off;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#CRM-Q10;
      services-crm:modules/entities/aws/api/record-locks.routes.ts:defineRecordLockRoutes;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
      the role at its version and lock, and records nothing;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the page ended;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403 naming sys_locked for
      a replacement that sets a lock (E-14);
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 IllegalOperation, not
      403, for a replacement of a contact locked against updates, the lock sent back unchanged;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 to a replacement that
      only clears an element lock without the unlock key, and 200 with it (CRM-ACC-0035);
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      the unlock, naming the protection; services-crm:docs/record-locks.md#Not covered yet;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
      exactly one revision, whatever the save touched;
      services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js#answers a caller
      with the history key but no read of organizations 404, as the organization itself would;
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#refuses a query parameter
      it does not know, rather than ignoring the filter;
      services-crm:modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js#still
      answers for a user who has been deleted, because former users are named too;
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#shows the role removed
      and the role added, each under the name it has now;
      services-crm:modules/entities/aws/api/contacts.routes.ts:absorbedContactHistoryRoutes;
      services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js#may
      not insert a revision, so nothing the application runs can forge one;
      services-crm:docs/record-history.md#Fail closed, and the maintenance helper;
      services-crm:docs/record-history.md#Retention
---

# Record Protection and History

## What It Is

Record Protection and History is the set of safeguards and retained explanations that surrounds contact and organization changes. Staff encounter it as a record version, archive state, lock, or revision timeline. These facets answer separate questions: whether an edit is current, whether the record is available for ordinary work, whether it can be changed, and what happened earlier.

Customer locks can also protect individual owned elements, and contact roles have supported lock administration. Rhythm's protected system records express a stronger authority than customer locks. The mechanism exists, but no current CRM seed is protected. Revisions capture attributed committed differences; they are evidence about changes, not editable replacements for live records.

## Why It Exists

Our reading is that durable identity data needs a trustworthy explanation of change as well as validation of its present values. Staff can open old information while another person or a worker changes it. A closed organization can still explain historical affiliations. Distinct safeguards let the association preserve context and control corrections without making every preservation decision a deletion or every lock irreversible.

## What It Is Not

A version is not a reservation that prevents another editor from working. A lock is not ordinary access permission: an authorized editor can still encounter a protected record. Archive is not a business status and does not erase relationships. A revision is not a recoverable backup or a restore command. Protected system configuration is also distinct from a customer lock that administrators can release.

## What It Controls

Versions govern whether field-changing requests are based on the current record. Real changes advance them, including relevant system changes; unchanged current saves do not. Locks constrain updates or deletion, and owned-element locks prevent parent operations from bypassing protected details. Dedicated archive actions govern availability independently of record status and preserve the record's existing information.

History commits attributed revisions and provides separately authorized reads, including retained evidence for deleted records. Customer lock actions record their own reasons and before-and-after restrictions. Protected system records refuse customer lock, unlock, and delete actions while ordinary fields can remain editable. These are complementary effects rather than one lifecycle flag that every operation interprets identically.

## How It Is Used

The CRM Editor saves against the version read and reloads or compares after a conflict. The CRM Administrator chooses archive, restore, deletion, or lock changes according to the actual need and authority. The CRM Reader inspects permitted revisions and current or archived information. Archive a closed organization and inspect its retained history illustrates preservation of an identity used by past work.

Good use keeps action and explanation connected. An administrator records why a lock changes, and a coordinator distinguishes a system-attributed update from a colleague's correction. An integration does not bypass a lock or archive simply because it uses the service. History supports investigation without promising that earlier information can be restored automatically.

## Example

Two coordinators edit Ruth's contact. One saves a corrected phone; the other's older title correction conflicts. Reloading preserves the first change and lets the second save deliberately. History then explains the committed edits rather than recording a silent overwrite or another entry for an unchanged round trip.

Separately, Harbor Engineering closes with ended affiliations still referring to it. An administrator archives it instead of deleting its context. Direct reads and past affiliations remain available, ordinary selection excludes it, and authorized restoration can make it available again. The revision explains that lifecycle action without changing the organization's business status.

## How It Relates

Contact and Organization carry versions, lifecycle state, and captured revisions. Affiliation and contact-detail elements participate in aggregate locks and changes rather than becoming independent protection concepts. Platform security owns authority evaluation, Platform identity supplies actor names, and Platform files owns storage retirement. Data Quality owns merge survivors. Workspace lifecycle owns purge, which defines the end of revision retention rather than ordinary record deletion.
