---
id: STATE-CRM-ENT-007
title: Record lock
object: DOM-CRM-ENT-033
product_area: crm
module: entities
revision: 1
status: draft
design_intent: confirmed
origin: >-
  D-08 (Andrew, 2026-10-19, Modified Option 1), cited in
  services-crm:docs/record-locks.md and CRM-M1 plan A17
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
  - name: unlocked
    meaning: Ordinary permissions allow updating and deleting the record.
    entry_conditions: No customer lock value applies to the subject.
    allowed_actions:
      - >-
        Ordinary read, update or delete with their grants and other record
        guards
      - dedicated lock.
    terminal: false
    reversible: true
    provenance: observed
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
    experience: >-
      There is no customer lock blocking mutation, but archive and protection
      can still apply.
  - name: delete-locked
    meaning: 'Updates remain allowed, but deletion is locked.'
    entry_conditions: A dedicated action selects the deletes restriction.
    allowed_actions:
      - Read
      - ordinary update under other guards
      - dedicated lock replacement or unlock.
    terminal: false
    reversible: true
    provenance: observed
    source: >-
      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
    experience: Deletion is refused while ordinary corrections can remain available.
  - name: fully locked
    meaning: Ordinary updates and deletion are both locked.
    entry_conditions: A dedicated action selects updates & deletes.
    allowed_actions:
      - Read
      - dedicated lock change or unlock
      - inspect lock history.
    terminal: false
    reversible: true
    provenance: observed
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a replacement that sets a lock (E-14)
    experience: >-
      Ordinary save and deletion are restricted, including relevant aggregate
      work.
  - name: protected
    meaning: >-
      An independent Rhythm protection entry prevents customer lock changes and
      deletion, while ordinary field editing remains allowed.
    entry_conditions: A Rhythm-maintained registry entry names the subject.
    allowed_actions:
      - Read
      - ordinary fields can remain editable under all other guards.
    terminal: false
    reversible: false
    provenance: observed
    source: >-
      services-crm at 7ea48a41: docs/record-locks.md;
      modules/entities/services/persistence/record-locks.ts;
      aws/api/record-locks.routes.ts, record-lock-api-mapper.ts;
      db/record-lock-changes.table.ts, protected-records.table.ts.
    experience: >-
      Customer lock, unlock and delete are refused with the protection
      explanation.
transitions:
  - from: unlocked
    to: delete-locked
    trigger: Apply a delete lock using the dedicated action.
    guard: >-
      Lock permission, matching version, nonblank reason within 1,000 characters
      and no Rhythm protection.
    side_effects:
      - advance version and modification stamps
      - append owner lock audit
      - capture contact or organization history and event
    who: An authorized lock operator
    provenance: test
    source: >-
      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
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
  - from: unlocked
    to: fully locked
    trigger: Apply a full update-and-delete lock.
    guard: Dedicated lock checks apply; ordinary saves cannot create a full lock.
    side_effects:
      - persist full lock
      - advance version and audit the change
    who: An authorized lock operator
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a replacement that sets a lock (E-14)
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
  - from: delete-locked
    to: fully locked
    trigger: Strengthen a delete lock to a full lock.
    guard: 'Use dedicated lock authorization, version and reason checks.'
    side_effects:
      - advance version
      - append the changed lock audit
    who: An authorized lock operator
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
  - from: fully locked
    to: unlocked
    trigger: Unlock the owner and selected or all owned elements.
    guard: >-
      Unlock permission, matching version, required reason, valid owned element
      references and no Rhythm protection.
    side_effects:
      - advance owner version when any lock changes
      - audit each released element and the owner lock if changed
    who: An authorized unlock operator
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
  - from: delete-locked
    to: unlocked
    trigger: Release a delete lock using the dedicated action.
    guard: Dedicated unlock checks apply.
    side_effects:
      - clear owner lock
      - advance version and append audit
    who: An authorized unlock operator
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
  - from: fully locked
    to: delete-locked
    trigger: Replace a full lock with a delete lock.
    guard: >-
      Lock action authorization applies; the current full lock does not block
      its dedicated change.
    side_effects:
      - advance version
      - audit replacement of the owner lock
    who: An authorized lock operator
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    audit_event: >-
      A changed dedicated lock action stores reason, actor, time and
      before-and-after locks, including released elements. Contact and
      organization changes also enter aggregate revision history; requesting an
      unchanged restriction with no released elements records no change.
invalid:
  - from: protected
    to: unlocked
    why: Customer lock and unlock actions refuse a Rhythm-protected subject.
    response: >-
      Customer unlock cannot remove Rhythm protection. The service returns an
      illegal-operation refusal describing the protected system record,
      regardless of customer grants.
  - from: unlocked
    to: protected
    why: >-
      Only migrations and operators maintain Rhythm protection; application
      access to its registry is read-only.
    response: >-
      An ordinary customer write cannot create a protection entry. The
      application has read-only access to that registry; only the operator or
      migration boundary can maintain it.
concurrency: >-
  Dedicated lock actions compare versions under the owner row lock and can
  conflict with concurrent saves. Full contact or affiliation locks defer
  automatic-role work. Legacy contact-role ordinary saves can change a delete
  lock without the dedicated permission, reason or audit; approved planning
  explicitly permits that exception.
invariants:
  - INV-DOM-CRM-ENT-033-04
  - INV-DOM-CRM-ENT-033-05
  - INV-DOM-CRM-ENT-033-06
  - INV-DOM-CRM-ENT-033-07
evidence:
  - >-
    services-crm at 7ea48a41: docs/record-locks.md;
    modules/entities/services/persistence/record-locks.ts;
    aws/api/record-locks.routes.ts, record-lock-api-mapper.ts;
    db/record-lock-changes.table.ts, protected-records.table.ts.
  - >-
    react-console at da630758: lib/record-views/views/contact-360.json (Lock,
    Unlock and Lock History actions); components/records/locks/*.
automatic: []
ai_may_recommend: false
ai_may_execute: false
permissions_by_state:
  - state: unlocked
    action: read
    permission: crm.contacts.read
  - state: unlocked
    action: lock
    permission: crm.contacts.lock
  - state: unlocked
    action: unlock
    permission: crm.contacts.unlock
  - state: unlocked
    action: read
    permission: crm.organizations.read
  - state: unlocked
    action: lock
    permission: crm.organizations.lock
  - state: unlocked
    action: unlock
    permission: crm.organizations.unlock
  - state: unlocked
    action: read
    permission: crm.contact-roles.read
  - state: unlocked
    action: lock
    permission: crm.contact-roles.lock
  - state: unlocked
    action: unlock
    permission: crm.contact-roles.unlock
  - state: delete-locked
    action: read
    permission: crm.contacts.read
  - state: delete-locked
    action: lock
    permission: crm.contacts.lock
  - state: delete-locked
    action: unlock
    permission: crm.contacts.unlock
  - state: delete-locked
    action: read
    permission: crm.organizations.read
  - state: delete-locked
    action: lock
    permission: crm.organizations.lock
  - state: delete-locked
    action: unlock
    permission: crm.organizations.unlock
  - state: delete-locked
    action: read
    permission: crm.contact-roles.read
  - state: delete-locked
    action: lock
    permission: crm.contact-roles.lock
  - state: delete-locked
    action: unlock
    permission: crm.contact-roles.unlock
  - state: fully locked
    action: read
    permission: crm.contacts.read
  - state: fully locked
    action: lock
    permission: crm.contacts.lock
  - state: fully locked
    action: unlock
    permission: crm.contacts.unlock
  - state: fully locked
    action: read
    permission: crm.organizations.read
  - state: fully locked
    action: lock
    permission: crm.organizations.lock
  - state: fully locked
    action: unlock
    permission: crm.organizations.unlock
  - state: fully locked
    action: read
    permission: crm.contact-roles.read
  - state: fully locked
    action: lock
    permission: crm.contact-roles.lock
  - state: fully locked
    action: unlock
    permission: crm.contact-roles.unlock
  - state: protected
    action: read
    permission: crm.contacts.read
  - state: protected
    action: read
    permission: crm.organizations.read
  - state: protected
    action: read
    permission: crm.contact-roles.read
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Purpose
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: Ownership and Authority
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: The Lifecycle
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: States
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: Transitions Explained
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: What Cannot Happen
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: Concurrency and Timing
    provenance: observed
    source: >-
      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/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - section: Permissions by State
    provenance: documented
    source: 'rhythm-v2-docs:specs/crm/AUTHORIZATION.md#Role Entitlement Matrix'
  - section: Purpose
    provenance: inferred
    source: >-
      services-crm at 7ea48a41: docs/record-locks.md;
      modules/entities/services/persistence/record-locks.ts;
      aws/api/record-locks.routes.ts, record-lock-api-mapper.ts;
      db/record-lock-changes.table.ts, protected-records.table.ts.
    note: >-
      The explicitly labeled rationale interprets the separation of state
      dimensions and the existing trigger and guard rows.
  - section: Invariants
    provenance: inferred
    source: >-
      INV-DOM-CRM-ENT-033-04; INV-DOM-CRM-ENT-033-05; INV-DOM-CRM-ENT-033-06;
      INV-DOM-CRM-ENT-033-07
    note: >-
      Protection explanations interpret the unchanged domain-object invariants;
      no lifecycle policy is added.
  - 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."
---

# Record lock

## Purpose

The lock lifecycle explains customer restrictions on record changes and the separate protection maintained by Rhythm. An association may allow corrections to a record while preventing deletion, or temporarily block both. Staff need to know whether an ordinary grant is enough, an audited unlock is required or the restriction belongs to Rhythm.

The recorded lock decision provides dedicated changes with reasons and an audit. Our reading is that treating protection as an independent overlay prevents customer administration from accidentally releasing configuration that the platform depends on. Ordinary field editability and protection against lock changes are therefore assessed separately.

## Ownership and Authority

Customer lock operators use the corresponding lock permission; unlock operators use the separately named unlock permission. The dedicated action checks version, reason and ownership of selected elements. A full lock does not forbid the very administrative action intended to change that lock. Ordinary saves cannot establish a new owner lock on contacts or organizations.

Migrations and operators maintain Rhythm protection, and application access to that registry is read-only. No model recommends or executes a lock transition in this module. The older contact-role save path remains a documented evidence exception to universal dedicated-lock auditing.

## The Lifecycle


```mermaid
stateDiagram-v2
  state "Customer lock (independent of protection)" as customerLock {
    [*] --> unlocked
    state "Delete locked" as deleteLocked
    state "Fully locked" as fullLocked
    unlocked --> deleteLocked: dedicated lock
    unlocked --> fullLocked: dedicated lock
    deleteLocked --> fullLocked: strengthen lock
    fullLocked --> deleteLocked: change lock
    deleteLocked --> unlocked: dedicated unlock
    fullLocked --> unlocked: dedicated unlock
  }
  note right of customerLock
    Rhythm protection independently blocks customer lock changes.
    Legacy role saves can change delete locks outside this action flow.
  end note
```

## States

### unlocked

Ordinary permissions allow updating and deleting the record. Entry requires this condition: No customer lock value applies to the subject. Available behavior is Ordinary read, update or delete with their grants and other record guards; dedicated lock.. There is no customer lock blocking mutation, but archive and protection can still apply.

### delete-locked

Updates remain allowed, but deletion is locked. Entry requires this condition: A dedicated action selects the deletes restriction. Available behavior is Read; ordinary update under other guards; dedicated lock replacement or unlock.. Deletion is refused while ordinary corrections can remain available.

### fully locked

Ordinary updates and deletion are both locked. Entry requires this condition: A dedicated action selects updates & deletes. Available behavior is Read; dedicated lock change or unlock; inspect lock history.. Ordinary save and deletion are restricted, including relevant aggregate work.

### protected

An independent Rhythm protection entry prevents customer lock changes and deletion, while ordinary field editing remains allowed. Entry requires this condition: A Rhythm-maintained registry entry names the subject. Available behavior is Read; ordinary fields can remain editable under all other guards.. Customer lock, unlock and delete are refused with the protection explanation.

## Transitions Explained

### unlocked → delete-locked

An authorized lock operator starts this change when the following occurs: Apply a delete lock using the dedicated action. The transition checks this boundary: Lock permission, matching version, nonblank reason within 1,000 characters and no Rhythm protection. The accompanying work is advance version and modification stamps; append owner lock audit; capture contact or organization history and event. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

### unlocked → fully locked

An authorized lock operator starts this change when the following occurs: Apply a full update-and-delete lock. The transition checks this boundary: Dedicated lock checks apply; ordinary saves cannot create a full lock. The accompanying work is persist full lock; advance version and audit the change. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

### delete-locked → fully locked

An authorized lock operator starts this change when the following occurs: Strengthen a delete lock to a full lock. The transition checks this boundary: Use dedicated lock authorization, version and reason checks. The accompanying work is advance version; append the changed lock audit. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

### fully locked → unlocked

An authorized unlock operator starts this change when the following occurs: Unlock the owner and selected or all owned elements. The transition checks this boundary: Unlock permission, matching version, required reason, valid owned element references and no Rhythm protection. The accompanying work is advance owner version when any lock changes; audit each released element and the owner lock if changed. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

### delete-locked → unlocked

An authorized unlock operator starts this change when the following occurs: Release a delete lock using the dedicated action. The transition checks this boundary: Dedicated unlock checks apply. The accompanying work is clear owner lock; advance version and append audit. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

### fully locked → delete-locked

An authorized lock operator starts this change when the following occurs: Replace a full lock with a delete lock. The transition checks this boundary: Lock action authorization applies; the current full lock does not block its dedicated change. The accompanying work is advance version; audit replacement of the owner lock. A changed dedicated lock action stores reason, actor, time and before-and-after locks, including released elements. Contact and organization changes also enter aggregate revision history; requesting an unchanged restriction with no released elements records no change.

## What Cannot Happen

Customer unlock cannot remove Rhythm protection. The service returns an illegal-operation refusal describing the protected system record, regardless of customer grants.

An ordinary customer write cannot create a protection entry. The application has read-only access to that registry; only the operator or migration boundary can maintain it.

## Permissions by State

The CRM model separates lock, unlock and read grants for contacts, organizations and contact roles. Administrator and Power User hold lock and unlock authority; Standard User and Read Only can inspect records but do not receive those administrative grants. Protection refuses customer lock changes regardless of the grant.

A customer full lock restricts ordinary mutation, while dedicated administrative changes remain available under their own permissions. A deletion-only lock leaves ordinary editing possible. Element locks and archive still add checks, so the absence of an owner restriction does not establish that every operation is allowed.

## Concurrency and Timing

The dedicated action locks its subject and compares the supplied read version before changing restrictions. A stale action receives a version conflict instead of overwriting a newer lock. Reasons and element references are validated before the successful change is committed with its audit.

A request for the current owner lock can still change the aggregate if it releases selected element locks. Full locks can delay scheduled affiliation work, and a later unlock does not establish a deadline for backlog recovery. Rhythm protection remains an independent check; the current seed contains no protected CRM records.

## Invariants

Our reading is that separating customer locks from Rhythm protection preserves the intended administrator boundary. Unlock authority can release a customer restriction, but cannot release platform protection. A required reason and committed before-and-after audit make actual lock changes explainable. Owner and nested-element restrictions can differ, so releasing an owner lock alone need not make every detail editable. The contact-role ordinary-save exception remains explicit rather than falsely extending dedicated-action audit guarantees to all role edits.
