---
status: proposed
related:
  - docs/profile/profile-refactor-design.md
  - docs/profile/roadmap-profile-convergence.md
  - docs/design/permission-system/composite-object-access-policy.md
  - backend/internal/controlplane/packs/platform_core_test.go
---

# CaseComment Delegated Access Policy Design

## Scope Statement

This doc is a **dedicated owner-model design** for `CaseComment`.

It is **not** a full closure of the broader `#1970` line.
It is **not** the environment-evidence tail for `OpportunityContactRole`.
It is the missing follow-up design that `docs/profile/profile-refactor-design.md`
explicitly left open for the delegated/special-surface bucket.

What this doc freezes:

1. the steady-state owner model for `CaseComment`
2. the generic metadata primitive family `CaseComment` should use
3. the runtime / seeder / repair / verify convergence rules that follow from
   that owner model

What this doc does **not** freeze:

1. environment execution evidence
2. rollout timing
3. destructive cleanup execution
4. portal/community/public-comment product nuances beyond the internal
   permission boundary needed for the current `#1970` line

Implementation precondition:

- `#2063`, or an equivalent branch, must land the generic
  `_System_Object.AccessPolicy` foundation first

## Purpose

`CaseComment` is the remaining object in the `profile` / `#1970` line whose
owner model is still unfrozen.

`#2051` intentionally kept it out of the execution-ready scope because
`CaseComment` does not look like either of the already-settled buckets:

- not a row-level-proven child whose self CRUD/FLS surface can be preserved
- not a pure composite object like `OpportunityContactRole`

The missing fact is that `CaseComment`'s product boundary depends on:

1. the parent `Case` permission surface
2. a case-comment-specific system permission
3. whether the current user authored the comment being edited/deleted

That makes it a **delegated access surface**, not a generic self-owned object.

## Salesforce Baseline and Candidate Paths

### Salesforce Baseline

Salesforce's public help and object reference support the following product
shape for `CaseComment`:

1. `CaseComment` is a standard child object attached to `Case`
2. viewing case comments follows the user's access to the parent case
3. adding a case comment is gated by the parent case surface, not by a
   standalone `CaseComment` self-owned CRUD slot
4. editing or deleting comments is not a pure object-CRUD decision; it depends
   on:
   - whether the user is editing their own comment
   - whether the user holds the dedicated `Edit Case Comments` permission
   - whether the user has stronger case-wide authority such as
     `Modify All Records` on `Case`

Authoritative Salesforce references used for this design:

- CaseComment object reference:
  <https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_casecomment.htm>
- Salesforce Help — Case Comments:
  <https://help.salesforce.com/s/articleView?language=en_US&id=sales.salesforce_case_comments.htm&type=5>
- Salesforce Help — Case Comments:
  <https://help.salesforce.com/s/articleView?language=en_US&id=sf.case_comments.htm&type=5>
- Salesforce Help — Case Comments:
  <https://help.salesforce.com/s/articleView?language=en_US&id=sf.support_case_comments.htm&type=5>

For the current line, the key baseline conclusion is:

> `CaseComment` is not a self-owned generic object-permission surface. Its
> access model is delegated from `Case` plus comment-specific predicates.

### Candidate Paths Considered

1. Keep `CaseComment` out of standard metadata forever.
   Rejected. The object is a real Salesforce standard object; permanent absence
   would preserve a repo gap rather than model the product correctly.

2. Ship `CaseComment` as a normal metadata-only child with generic self
   `ObjectPermissions` / self `FieldPermissions`.
   Rejected. That silently invents a self-owned permission surface that the
   product boundary does not actually expose.

3. Reuse `OpportunityContactRole`'s pure composite `AccessPolicy` shape
   unchanged.
   Rejected. OCR needs only canonical object-permission composition; it does
   not need an actor-sensitive distinction such as "own comment" vs
   "someone else's comment".

4. Add a separate `_System_Object.DelegatedAccessPolicy` primitive.
   Rejected. That would create a second owner-model storage surface when
   `_System_Object.AccessPolicy` already exists for non-self permission
   ownership.

5. Reuse `_System_Object.AccessPolicy` as the single owner-model family and
   extend its expression grammar with one generic actor-sensitive delegated
   term.
   Chosen. This preserves one metadata fact, one evaluator family, and one
   runtime/seeder/repair/verify predicate for "this object no longer owns self
   OP/FP rows."

## Repo Evidence Checked

1. The current `#2051` contract already classifies `CaseComment` as a
   delegated/special-surface object rather than a row-level-proven child:
   `docs/profile/profile-refactor-design.md:129`
2. The current `#2051` staged design explicitly freezes `CaseComment` outside
   the execution-ready scope and requires a dedicated follow-up design before
   implementation:
   `docs/profile/profile-refactor-design.md:323`
   `docs/profile/profile-refactor-design.md:590`
3. Platform-core guardrail tests already prevent `CaseComment` from re-entering
   the metadata-only cutover as a fake child-owned object:
   `backend/internal/controlplane/packs/platform_core_test.go:562`
4. The existing composite-access design already established the reusable
   `_System_Object.AccessPolicy` family as the generic answer for non-self
   permission surfaces, and explicitly called out `CaseComment`-class follow-up
   work on that same primitive family:
   `docs/design/permission-system/composite-object-access-policy.md:44`
   `docs/design/permission-system/composite-object-access-policy.md:196`
   `docs/design/permission-system/composite-object-access-policy.md:301`
5. The corresponding implementation line `#2063` already uses
   `_System_Object.AccessPolicy` as the single owner fact for
   non-self-owned objects such as `OpportunityContactRole`; that is the
   foundation this design builds on, not a new parallel mechanism.

## Root Cause

The current generic platform can model two kinds of access surfaces:

1. self-owned object/field access via `ObjectPermissions` / `FieldPermissions`
2. pure derived/composite access via canonical permission composition

`CaseComment` needs one more capability:

- access predicates that are still metadata-authored and generic, but can
  distinguish between:
  - the current user's own comment
  - another user's comment

Without that capability, the platform is forced into one of two wrong states:

1. invent a fake self-owned `CaseComment` permission surface
2. add `if objectName == "CaseComment"` logic to runtime code

Both violate the `CLAUDE.md` baseline.

## Chosen Owner Model

Freeze `CaseComment` as:

- row-level visibility: parent-derived from `Case`
- object CRUD / field access: metadata-authored delegated policy on
  `_System_Object.AccessPolicy`
- self `ObjectPermissions` / self `FieldPermissions`: not authoritative in the
  steady state

This means `CaseComment` joins the same **owner family** as OCR:

- one metadata fact on `_System_Object`
- one evaluator family
- one convergence rule for runtime / seeder / repair / verify

But it uses a richer delegated expression shape than OCR.

## Proposed Metadata Shape

Keep using `_System_Object.AccessPolicy`.

Do **not** add a new top-level policy column.

### Required Generic Extension

The current `AccessPolicy` expression family needs one additional generic term:

- `recordFieldEqualsCurrentUser`
  - compares a record field such as `CreatedById` to the acting user

It also needs to treat `ModifyAllRecords` on a referenced object as a canonical
permission operation in the same family as `Read`, `Create`, and `Edit`.

### Suggested Shape

```json
{
  "mode": "derived",
  "objectRead": {
    "kind": "objectPermission",
    "object": "Case",
    "operation": "Read"
  },
  "objectCreate": {
    "kind": "objectPermission",
    "object": "Case",
    "operation": "Edit"
  },
  "objectEdit": {
    "anyOf": [
      { "kind": "objectPermission", "object": "Case", "operation": "ModifyAllRecords" },
      {
        "allOf": [
          { "kind": "systemPermission", "apiName": "EditCaseComments" },
          { "kind": "recordFieldEqualsCurrentUser", "field": "CreatedById" }
        ]
      }
    ]
  },
  "objectDelete": {
    "anyOf": [
      { "kind": "objectPermission", "object": "Case", "operation": "ModifyAllRecords" },
      {
        "allOf": [
          { "kind": "systemPermission", "apiName": "EditCaseComments" },
          { "kind": "recordFieldEqualsCurrentUser", "field": "CreatedById" }
        ]
      }
    ]
  },
  "fieldRead": { "inherit": "objectRead" },
  "fieldEdit": { "inherit": "objectEdit" }
}
```

`objectCreate` has now been re-checked against Salesforce Help's documented
permission contract for case comments:

> "Users need the Read and Edit Cases permissions to view, create, edit, and
> delete case comments."

That keeps the delegated create gate aligned with the current metadata shape:

- `Case.Read` remains the visibility baseline via `objectRead`
- `Case.Edit` is the minimum documented parent-object write gate for creating a
  comment
- the shipped metadata should therefore require `Case.Edit` directly, rather
  than widening create to `Case.Create OR Case.Edit`

If future Salesforce product evidence shows a stricter create boundary than the
current help baseline, the metadata expression and this doc must be updated in
the same slice.

### Why `mode = derived` still stays correct

Do **not** invent a second owner-mode bit such as `mode = delegated`.

For the current platform, the important owner fact is:

> does this object own self `ObjectPermissions` / self `FieldPermissions`, or
> is its access derived from another metadata-authored policy?

For `CaseComment`, the answer is still "derived from metadata-authored policy."

Keeping `mode = derived` preserves the single owner predicate already converged
by the `#2063` line. The delegated behavior lives in the expression grammar,
not in a second top-level ownership mode.

## Runtime Design

### 1. Standard Metadata

Once the dedicated implementation follow-up starts:

- add `CaseComment` to `standard_objects_spec.json`
- declare its parent relationship to `Case`
- keep row-level visibility parent-derived
- attach the delegated `AccessPolicy`

Until then, the current guardrail that keeps `CaseComment` out of the
metadata-only cutover remains correct.

### 2. One Evaluator Family

Reuse the existing `AccessPolicy` evaluator family.

Required additions:

1. support `objectDelete`
2. support `ModifyAllRecords` as a canonical object-permission operation
3. support `recordFieldEqualsCurrentUser`

Do not create:

- a CaseComment-only evaluator
- a CaseComment-only CRUD branch
- a separate delegated-policy table

### 3. Existing Entry Points

The following surfaces must continue to consume the same owner model:

- object CRUD gate
- field read/edit gate
- global describe / object discoverability
- query / analytics access gate
- record access after object gate succeeds

The evaluator may become richer, but the entrypoint pattern stays the same:

> same fact, same helper, same owner predicate

### 4. Row-Level Visibility

Row-level visibility still follows the parent `Case`.

This design does **not** invent a second row-sharing model for comments.

The delegated part is the object/field gate, not the record-visibility gate.

## Seeder / Repair / Verify Convergence

Once `CaseComment` is promoted into the standard spec with the delegated
`AccessPolicy`:

1. seeder must stop generating self `ObjectPermissions` / self
   `FieldPermissions` rows for `CaseComment`
2. repair/reconcile must not backfill those self rows as if they were missing
3. verify/profile coverage must exclude `CaseComment` from expected self-row
   counts for the same owner predicate used by OCR
4. any historical self-row cleanup must live in `data-specs/`, not in
   long-lived runtime compatibility logic

This is non-negotiable. A delegated policy with self-row seeding still active
would recreate the same split-SSoT failure mode that `#1970` is trying to
eliminate.

## Declared Delta from Salesforce

The declared delta is storage/evaluator shape only:

- Salesforce does not expose its internal implementation as
  `_System_Object.AccessPolicy`
- NexusCRM will store the same product contract as metadata-authored policy
  expressions
- for the current line, field access inherits the delegated object gate unless
  stronger Salesforce evidence later proves an independent admin-authored field
  surface that must be modeled separately

The external product boundary should remain:

- comments are visible through case access
- creation is case-gated, not self-object-gated
- own-comment edit/delete is delegated through the dedicated permission
- stronger case-wide authority can override that boundary generically

This is an allowed engine-storage delta, not a business-rule delta.

## Next-Tenant Impact

The next tenant should get:

- the same standard `CaseComment` product boundary
- no object-name branch
- no fake self-owned permission rows
- one reusable delegated-access pattern for the next standard object that
  depends on actor-sensitive predicates

This is better than solving `CaseComment` with a one-off patch because it
keeps future delegated objects in metadata instead of code.

## Verification Plan

The implementation PR for this design must include:

1. evaluator unit tests for:
   - `recordFieldEqualsCurrentUser`
   - `ModifyAllRecords`
   - `objectDelete`
   - mixed `allOf` / `anyOf` delegated expressions
2. object-surface tests proving:
   - `CaseComment` is absent from object discovery without `Case.Read`
   - create requires the correct case gate
   - own-comment edit/delete succeeds only with `EditCaseComments`
   - editing/deleting another user's comment requires the stronger
     case-wide authority branch
3. row-level tests proving:
   - after object access passes, record visibility still follows the parent
     `Case`
4. seeder tests proving:
   - delegated-policy `CaseComment` skips self OP/FP rows
5. verify/reconcile tests proving:
   - delegated-policy `CaseComment` is excluded from self-row expectations
   - repair does not recreate those rows
6. guardrail tests proving:
   - `CaseComment` cannot re-enter as a fake metadata-only child
7. if historical rows exist, one dedicated `data-specs/` cleanup spec plus
   pre/post verification

## Frozen Contract

This design freezes the following contract now:

1. `CaseComment` is **not** a generic self-owned child object
2. `CaseComment` should reuse `_System_Object.AccessPolicy`, not invent a
   second owner-model storage surface
3. the owner predicate for runtime / seeder / repair / verify remains aligned
   with the `AccessPolicy` family, not with self `ObjectPermissions` /
   `FieldPermissions`
4. delegated actor-sensitive behavior belongs in the `AccessPolicy` expression
   grammar, not in L0 object-name branches
5. field access for the current line inherits the delegated object gate unless
   a later design-doc PR brings stronger Salesforce evidence for a separate
   field-surface owner
6. `CaseComment` may only enter execution mode after the delegated-policy
   follow-up implementation is reviewed against this contract

## Out of Scope / Deferred

Still out of scope for this doc:

1. environment execution evidence
2. portal/community/public-comment experience details that are not required to
   freeze the internal owner model
3. cleanup execution timing
4. any unrelated profile / verify / staging noise

## Contract Update Protocol

Any future change to this owner model requires a design-doc PR that answers:

1. what Salesforce baseline changed or was clarified?
2. what repo evidence changed?
3. why is the current delegated `AccessPolicy` family insufficient?
4. what changes for the next tenant?
5. how will runtime / seeder / repair / verify stay on one owner predicate?

Implementation PRs may realize this design.
They may not silently replace it.
