---
id: WF-CRM-ENT-009
title: Delete a contact or an organization
trigger: >-
  An authorized user deletes a contact or organization, or a bulk job or merge
  invokes the same persistence boundary.
product_area: crm
module: entities
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
inference: []
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
implements:
  - CAP-CRM-ENT-014
  - CAP-CRM-ENT-015
  - CAP-CRM-ENT-016
moves:
  - STATE-CRM-ENT-001
  - STATE-CRM-ENT-002
crosses:
  - SVC-CRM-ENT-entities
idempotency: >-
  Deletion has no version argument. Repeating a committed delete returns not
  found, or merged-identity resolution where applicable. Cleanup and reactions
  handle an already-absent subject without recreating it.
concurrency: >-
  Owner row locking orders deletion against an in-flight save. Reciprocal spouse
  cleanup is part of the same contact transaction. Locking coordinates
  concurrent writes without promising every multi-record operation is
  deadlock-free.
hops:
  - 'n': 1
    actor_or_service: Entities route
    action: >-
      Require the administrator-tier delete permission and resolve a merged
      identity to its survivor where applicable.
    anchor: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
    seam: sync
    on_failure: >-
      Return authorization, absent-record or merged-record refusal before
      deletion.
    left_inconsistent: No partial business write remains.
    observer: The caller receives the result.
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0046.spec.ts#CRM-ACC-0046
  - 'n': 2
    actor_or_service: Entities persistence (one transaction)
    action: >-
      Lock the owner and check Rhythm protection, owner and element locks, and
      organization children or affiliations.
    anchor: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
    seam: sync
    on_failure: >-
      Return the blocking protection, lock or dependency without removing the
      record.
    left_inconsistent: No partial business write remains.
    observer: The caller receives the result.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked
  - 'n': 3
    actor_or_service: Platform associations (inside the same transaction)
    action: >-
      When enabled, call Platform’s endpoint-deletion function in the CRM
      transaction to apply detach, cascade or restrict policy.
    anchor: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
    seam: sync
    on_failure: A restrictive association policy aborts the entire deletion.
    left_inconsistent: >-
      When disabled, associated custom records can retain references to the
      removed endpoint.
    observer: Association policy errors and remaining Platform data show the gap.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/persistence/endpoint-deletions.spec.js#never
      runs a statement when it is off
  - 'n': 4
    actor_or_service: Entities persistence (same transaction)
    action: >-
      Clear reciprocal spouse references with system attribution, remove the
      owner and cascading children, settle duplicate bookkeeping and enabled
      attention, detach files and remove source mappings.
    anchor: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
    seam: sync
    on_failure: Roll back the complete delete transaction if any step fails.
    left_inconsistent: No partial business write remains.
    observer: The caller receives the result.
    provenance: test
    source: >-
      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)
  - 'n': 5
    actor_or_service: Database change capture
    action: >-
      Capture the deletion event and historical revision independently of a live
      owner reference.
    anchor: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
    seam: sync
    on_failure: Capture failure aborts deletion.
    left_inconsistent: No partial business write remains.
    observer: History permission permits inspecting the retained deletion revision.
    provenance: test
    source: >-
      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
  - 'n': 6
    actor_or_service: CRM change-event consumers
    action: >-
      Recalculate organizations for a deleted contact and process search
      removal, duplicate and region reactions.
    anchor: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
    seam: queue
    on_failure: 'Retry failed handler messages, then dead-letter after five receives.'
    left_inconsistent: >-
      Search can still show deleted data and organization compliance can remain
      stale.
    observer: Consumer logs and dead-letter messages identify failed reactions.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-change-events.integration.spec.js#a
      contact saved with the notes DISABLE_CONTACT_ROLE_COMPLIANCE_CHECK:
      nothing (its delete still records)
  - 'n': 7
    actor_or_service: Asset cleanup sweep and consumer
    action: >-
      Claim detached files on the 15-minute sweep, queue cleanup and recheck
      references before retiring files into expiring deleted storage.
    anchor: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
    seam: schedule
    on_failure: >-
      Retry cleanup messages; terminal failures retain failed rows and
      dead-letter messages.
    left_inconsistent: Files remain until a successful cleanup and later storage expiry.
    observer: >-
      Cleanup logs, state rows and its dead-letter queue expose unresolved
      files.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/assets/retire-entity-assets.integration.spec.js#detaches
      the photo in the delete's own transaction
branches:
  - >-
    Bulk jobs apply the same record checks and report each refusal as a
    rejection.
  - >-
    Merge uses the delete path and refuses a loser whose enabled owner-deletion
    policy would remove associated records.
waits:
  - >-
    The 15-minute sweep establishes cleanup eligibility; retries or references
    can defer actual retirement.
evidence:
  - >-
    services-crm at 7ea48a41: modules/entities/services/persistence/contacts.ts
    (deleteContact, deleteContactRow), organizations.ts (deleteOrganization,
    deleteOrganizationRow), endpoint-deletions.ts; aws/crm.names.ts
    (CRM_OWNER_DELETION_RULES_BY_ENVIRONMENT);
    aws/logic/entities-asset-cleanup.lambda.ts; docs/record-history.md
    (Retention).
  - >-
    services-platform at ad3f6a65: docs/assets.md (Cleanup);
    docs/queued-work.md.
controls:
  - TC-AZ-001
  - TC-AZ-002
  - TC-AZ-003
  - TC-AZ-004
  - TC-AZ-005
  - TC-AZ-006
  - TC-DAT-001
  - TC-DAT-002
  - TC-DAT-003
  - TC-DAT-004
  - TC-IAM-001
  - TC-IAM-005
  - TC-IAM-006
  - TC-LOG-001
  - TC-LOG-002
  - TC-LOG-003
  - TC-LOG-004
  - TC-LOG-005
  - TC-MON-001
  - TC-MON-002
  - TC-PI-001
  - TC-PI-003
  - TC-PI-004
  - TC-PI-005
  - TC-PRV-001
  - TC-PRV-002
  - TC-PRV-003
  - TC-TEN-001
  - TC-TEN-002
  - TC-TEN-003
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: What It Does
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: What It Does
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: What It Does
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: What It Does
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: What It Does
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: What It Does
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: What It Does
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: What Starts It
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: What Starts It
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: What Starts It
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: What Starts It
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: What Starts It
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: What Starts It
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: What Starts It
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: The Flow
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: The Flow
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: The Flow
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: The Flow
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: The Flow
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: The Flow
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: The Flow
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: Hop by Hop
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: Hop by Hop
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: Hop by Hop
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: Hop by Hop
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: Hop by Hop
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: Hop by Hop
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: Hop by Hop
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: When Something Fails
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: When Something Fails
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: When Something Fails
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: When Something Fails
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: When Something Fails
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: When Something Fails
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: When Something Fails
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: Repeats and Races
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: Repeats and Races
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: Repeats and Races
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: Repeats and Races
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: Repeats and Races
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: Repeats and Races
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: Repeats and Races
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: What Is Observable
    provenance: observed
    source: 'services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes'
  - section: What Is Observable
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact
  - section: What Is Observable
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion
  - section: What Is Observable
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow
  - section: What Is Observable
    provenance: observed
    source: 'services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions'
  - section: What Is Observable
    provenance: observed
    source: >-
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler
  - section: What Is Observable
    provenance: observed
    source: >-
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
  - section: What It Does
    provenance: inferred
    source: >-
      services-crm:modules/entities/aws/api/contacts.routes.ts:contactsRoutes;
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContact;
      services-crm:modules/entities/services/persistence/endpoint-deletions.ts:applyEndpointDeletion;
      services-crm:modules/entities/services/persistence/contacts.ts:deleteContactRow;
      services-crm:modules/entities/db/record-revisions.table.ts:recordRevisions;
      services-crm:modules/entities/services/organizations/compliance/compliance-change-events.ts:organizationComplianceRecalculationHandler;
      services-crm:modules/entities/aws/logic/entities-asset-cleanup.lambda.ts:createEntitiesAssetCleanupFunction
    note: >-
      The explicitly labeled rationale interprets the existing transaction,
      transport and recovery boundaries; it is not a product decision.
  - section: Open Design Questions
    provenance: documented
    source: 'rhythm-v2-docs:specs/crm/entities/FINDINGS.md#Findings'
---

# Delete a contact or an organization

## What It Does

Deletion removes a live contact or organization and its owned information while retaining the explanation of the removal. An administrator removing an erroneous person expects its details and affiliations to disappear together; deleting an organization also requires proving that children and people no longer depend on its identity.

Our reading is that the workflow needs separate boundaries for dependency policy, committed deletion and file retirement. Captured history survives the live owner, while downstream search removal and eventual asset cleanup can lag. Archive remains a different capability when staff need to preserve an identity for historical relationships.

## What Starts It

An authorized administrator or record operation asks to remove a live person or organization. Bulk and merge work can enter the same persistence boundary. The caller’s request starts dependency and restriction checks; it does not establish permission to erase every external projection or retained revision. An organization with remaining children or affiliations requires resolution of those dependencies first.

## The Flow

The diagram shows the sequence and service seams. Each synchronous stage waits for its result, while captured-event, queue and scheduled stages can run after the originating request has returned. The hop descriptions below identify the write and failure boundary, so the last arrow must not be mistaken for one atomic transaction across every participant.

```mermaid
sequenceDiagram
  participant U as Delete caller
  participant C as CRM persistence
  participant D as CRM database
  participant P as Enabled association policy
  participant Q as Reactions and cleanup
  U->>C: delete record
  C->>D: lock and check protection and dependencies
  opt owner policy enabled
    C->>P: apply policy in transaction
  end
  C->>D: remove aggregate and capture deletion
  D-->>U: commit delete result
  D->>Q: later projections and file retirement
```

## Hop by Hop

### 1. Entities route

The route requires the record’s administrative delete grant and recognizes merged or absent identities before persistence. A merged-id response points the caller toward the survivor; it is not authority to delete that survivor silently. Record access and the route’s grant remain the entry checks.

### 2. Entities persistence (one transaction)

Persistence locks the subject and checks owner restrictions, nested deletion locks and Rhythm protection. For an organization it also checks children and affiliations of every date. A blocking dependency prevents the local delete rather than cascading people’s relationships into missing organizational context.

### 3. Platform associations (inside the same transaction)

When enabled, Platform association deletion policy runs inside the same transaction and applies restrict, detach or cascade decisions. A restrictive policy rolls back the whole removal. When this integration is disabled, the flow does not establish cleanup of every external custom-record association.

### 4. Entities persistence (same transaction)

The transactional removal clears a surviving spouse with system attribution and deletes owner and cascading children. It also settles applicable duplicate bookkeeping and attention, detaches file references and removes source mappings. These local effects either commit together or roll back together.

### 5. Database change capture

Database capture retains the subject’s deletion explanation without requiring a live foreign-key owner. Missing capture attribution aborts the transaction. Authorized history inspection can therefore examine the removed aggregate even when ordinary record reads find nothing.

### 6. CRM change-event consumers

Consumer messages recalculate organizations affected by a deleted person and remove or refresh the relevant projections. A failing search or compliance reaction can leave a stale dependent view. Queue retries apply to that reaction rather than recreating or repeating the successful record deletion.

### 7. Asset cleanup sweep and consumer

The asset sweep claims detached file work and queues retirement. Before deleting a file, the consumer checks references again and moves eligible content into expiring deleted storage. Failure leaves observable cleanup state and queue evidence; removing a CRM record is not instant physical erasure of every file.

## When Something Fails

### Hop 1 — Entities route

The failure boundary is: Return authorization, absent-record or merged-record refusal before deletion. The remaining work or data is: No partial business write remains. The caller receives the result.

### Hop 2 — Entities persistence (one transaction)

The failure boundary is: Return the blocking protection, lock or dependency without removing the record. The remaining work or data is: No partial business write remains. The caller receives the result.

### Hop 3 — Platform associations (inside the same transaction)

The failure boundary is: A restrictive association policy aborts the entire deletion. The remaining work or data is: When disabled, associated custom records can retain references to the removed endpoint. Association policy errors and remaining Platform data show the gap.

### Hop 4 — Entities persistence (same transaction)

The failure boundary is: Roll back the complete delete transaction if any step fails. The remaining work or data is: No partial business write remains. The caller receives the result.

### Hop 5 — Database change capture

The failure boundary is: Capture failure aborts deletion. The remaining work or data is: No partial business write remains. History permission permits inspecting the retained deletion revision.

### Hop 6 — CRM change-event consumers

The failure boundary is: Retry failed handler messages, then dead-letter after five receives. The remaining work or data is: Search can still show deleted data and organization compliance can remain stale. Consumer logs and dead-letter messages identify failed reactions. This failure is downstream of committed input; operators investigate its own delivery rather than treating the caller’s original save as failed.

### Hop 7 — Asset cleanup sweep and consumer

The failure boundary is: Retry cleanup messages; terminal failures retain failed rows and dead-letter messages. The remaining work or data is: Files remain until a successful cleanup and later storage expiry. Cleanup logs, state rows and its dead-letter queue expose unresolved files. A later attempt can be required; the scheduling or publication boundary does not establish an immediate user-visible completion.

## Repeats and Races

Delete has no version argument. A second request after success receives not-found or merged-identity handling rather than a recognized replay response. Owner locking orders it against an in-flight save, and reciprocal spouse cleanup shares the contact transaction. These properties do not imply compare-and-delete semantics or universal deadlock freedom.

Bulk jobs use the same record guards and report refusals per item. Merge uses the persistence boundary but must also respect the enabled association policy. Cleanup and downstream reactions tolerate an already-absent subject without recreating it. Files remain until successful retirement and later storage expiry.

## What Is Observable

The caller receives the result. Association policy errors and remaining Platform data show the gap. History permission permits inspecting the retained deletion revision. Consumer logs and dead-letter messages identify failed reactions. Cleanup logs, state rows and its dead-letter queue expose unresolved files.

The person or initiating caller sees the synchronous result at its own boundary. Captured history establishes committed local changes; logs, durable job state and queue outcomes explain remaining work. A successful database change, accepted downstream request and completed delivery are different observations and must be investigated through the component that owns each one.

## Open Design Questions

The enabled association-deletion boundary and post-commit projection or file recovery limits remain existing findings. Ordinary deletion is not workspace purge, and retained revisions intentionally survive it. The evidence does not support a promise that every external association, index entry or physical file disappears with the response.
