---
status: proposed
related:
  - docs/design/permission-system/composite-object-access-policy.md
  - docs/design/permission-system/casecomment-delegated-access-policy.md
  - docs/profile/profile-refactor-design.md
  - backend/internal/features/permissions/access_policy.go
  - backend/internal/features/permissions/permission_seeder.go
  - backend/internal/features/permissions/record_access.go
  - backend/internal/features/permissions/rls_filter.go
  - backend/internal/features/permissions/service.go
  - backend/internal/controlplane/packspec/standard_objects_spec.json
  - shared/pkg/models/metadata.go
---

# Access Ownership Model

## Purpose

This document explains the abstraction shift behind the later `#1970`
permission work:

- keep the `#1970` goal of a unified, metadata-driven permission model
- keep `ControlledByParent` as the Salesforce-native primitive for row-level
  inheritance
- avoid forcing every object into a parent-only access story
- explain why the platform now needs an explicit **access ownership model**
  plus a metadata-authored `AccessPolicy` substrate

Those more specific contracts remain in:

- `docs/design/permission-system/composite-object-access-policy.md`
- `docs/design/permission-system/casecomment-delegated-access-policy.md`
- `docs/profile/profile-refactor-design.md`

## Scope And Non-Goals

Use this document for the abstraction layer:

- why `ControlledByParent` stayed important
- why parent-only access turned out to be too narrow
- why the platform now talks in terms of ownership modes

This document does **not**:

- replace the execution contracts in the profile docs
- freeze a final CRUD/FLS owner verdict for row-level-proven line-item objects
- reopen the detailed OCR or CaseComment design docs
- redefine the current staged profile rollout plan
- claim that the current repository's row-level readers have already converged
  to the pure Salesforce `ControlledByParent` boundary

Use the more specific docs for execution-contract details:

- `composite-object-access-policy.md`
  - detailed OCR design and metadata shape
- `casecomment-delegated-access-policy.md`
  - detailed CaseComment delegated owner model
- `profile-refactor-design.md`
  - staged profile/permission convergence contract and rollout boundaries

## Executive Summary

`#1970` identified the right strategic problem:

> the platform should not rely on object-name registries or bespoke runtime
> branches to explain access for implicit or special child objects.

What changed later was the abstraction level.

The reusable answer is **not**:

- "every child object should derive all access from one parent through
  `ControlledByParent`"

The reusable answer is:

- "every access gate should have one ownership model, declared in metadata,
  and runtime / seeder / repair / verify should all read that same contract"

That higher-level abstraction is the **access ownership model**.

In the current platform, the important ownership modes are:

1. `self-owned`
2. `parent-derived`
3. `composite-derived`
4. `delegated-derived`

`AccessPolicy` is the metadata-authored mechanism for the non-self-owned
object/field access modes.

## Design Review Gate Summary

This document is intentionally organized around the repository's design-review
questions:

1. Salesforce baseline
   - `ControlledByParent` is the native row-level primitive
   - some standard objects still expose non-self object/field access shapes
2. Candidate paths
   - pure parent-only
   - preserve self-owned everywhere
   - object-name runtime branches
   - ownership-model abstraction plus metadata-authored `AccessPolicy`
3. Repo evidence
   - current runtime, metadata, and seeder paths already distinguish
     row-level traversal from derived object/field access
4. Declared delta
   - metadata-shape delta for non-self ownership expression
5. Next-tenant impact
   - future objects can reuse ownership modes instead of forcing new L0 cases
6. Verification
   - current repository evidence is pinned by metadata, runtime, and guardrail
     tests listed below

The detailed object-level execution contracts still live in the dedicated OCR,
CaseComment, and profile convergence docs.

## What This Doc Is Not Changing

This document does **not** change the repository's high-level permission
principles:

1. object access, field access, and record visibility remain distinct gates
2. permission facts remain metadata-authored rather than hardcoded in L0
3. runtime should remain a generic evaluator over metadata
4. one fact should have one owner and one read family

The narrower refinement is:

> for some objects, the owner of the object/field access surface is not the
> object itself, and the platform needs a first-class way to model that fact.

## Relationship to Metadata-Driven Architecture

The relationship between the current design and the repository's
metadata-driven rules is:

1. metadata-driven is the architectural rule
   - business behavior should live in metadata or data, not object-name code
2. the access ownership model is the permission-domain abstraction
   - it answers: who owns the access surface for this gate?
3. `AccessPolicy` is the metadata substrate for non-self ownership
   - when the owner is not "self `ObjectPermissions` / `FieldPermissions`",
     the platform still needs a declarative metadata contract

So the relationship is not:

- metadata-driven on one side, `AccessPolicy` as a local workaround on the
  other

It is:

- metadata-driven is the platform rule
- the access ownership model is the domain abstraction built under that rule
- `AccessPolicy` is the metadata mechanism that keeps non-self ownership
  metadata-driven in practice

## Why `#1970` Was Directionally Right

The original `#1970` line got three things right:

1. access should converge toward one metadata-driven interpretation
2. runtime, seeder, repair, and verify should not each invent their own owner
   facts
3. platform code should read generic metadata shape rather than branch on
   object API names

Those points remain correct.

What changed is the answer to a narrower question:

> what is the right reusable primitive to unify around?

The repository work after `#1970` showed that a pure parent-only primitive was
too narrow for the full standard-object set.

## Salesforce Baseline And Candidate Paths

### Salesforce Baseline

Salesforce gives a strong native primitive for row-level inheritance:

- a child object can declare `SharingModel = ControlledByParent`
- a relationship field can act as the sharing parent
- record visibility then follows the parent record

That is the correct baseline for:

- row-level visibility
- query/RLS semantics
- parent-derived record access

Relevant Salesforce references:

- Salesforce Data Model Notation:
  <https://developer.salesforce.com/docs/platform/data-models/guide/salesforce-data-model-notation.html>
- Salesforce Mobile SDK master-detail discussion:
  <https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/entity_framework_sync_related.htm>

The design lesson is that `ControlledByParent` naturally answers:

> who can see this child record, given access to the parent record?

It does **not** automatically answer:

- object CRUD ownership
- field-level ownership
- actor-sensitive edit/delete predicates
- objects whose access surface is a composition of multiple canonical objects

So `ControlledByParent` remains a Salesforce-native primitive for
**row-level inheritance**, not a complete ownership algebra for every access
surface.

### Candidate Paths Considered

1. Treat `ControlledByParent` as the universal answer for row-level, object
   CRUD, and field FLS ownership.
   Rejected. This over-claims what Salesforce sharing metadata proves and
   collapses object/field product distinctions that are still real for some
   standard objects.

2. Preserve self-owned `ObjectPermissions` / `FieldPermissions` as the answer
   for every object shape, even when the product boundary is composite or
   delegated.
   Rejected. This forces the wrong owner fact onto OCR / CaseComment-class
   objects and keeps the platform from modeling non-self ownership truthfully.

3. Handle special objects through runtime object-name branches.
   Rejected. This violates the metadata-driven boundary and turns product facts
   into L0 code.

4. Treat access as an ownership-model problem:
   - keep `ControlledByParent` for row-level inheritance
   - use `self-owned`, `parent-derived`, `composite-derived`, and
     `delegated-derived` as the reusable vocabulary
   - use metadata-authored `AccessPolicy` for non-self-owned object/field
     access surfaces
   Chosen. This preserves Salesforce's layered contract while keeping the
   runtime generic.

## Repo Evidence Checked

The current repository already reflects the split this document describes.

### A. Current row-level readers already centralize `ControlledByParent` traversal

1. record access delegates `ControlledByParent` objects through a parent access
   walk in the generic reader path:
   `backend/internal/features/permissions/record_access.go:101`
2. query/RLS builds a parent-derived filter for `ControlledByParent` objects
   in the generic RLS builder path:
   `backend/internal/features/permissions/rls_filter.go:108`

### B. Metadata already models non-self-owned object/field access through `AccessPolicy`

1. object metadata already carries `AccessPolicy`, with `mode=derived` and
   recursive expression terms:
   `shared/pkg/models/metadata.go:214`
   `shared/pkg/models/metadata.go:232`
   `shared/pkg/models/metadata.go:276`
2. the permission service already dispatches derived objects through
   `evaluateObjectAccessPolicy(...)` instead of self object permissions:
   `backend/internal/features/permissions/service.go:327`
3. field edit and field read already follow the same derived-vs-self split:
   `backend/internal/features/permissions/service.go:447`
   `backend/internal/features/permissions/service.go:489`
   `backend/internal/features/permissions/service.go:457`
   `backend/internal/features/permissions/service.go:499`

### C. Secondary layers already key off the same ownership fact

1. permission seeding skips derived-policy objects entirely:
   `backend/internal/features/permissions/permission_seeder.go:51`
   `backend/internal/features/permissions/permission_seeder.go:209`
2. shipped standard metadata already declares OCR and CaseComment as
   `sharingModel=ControlledByParent` plus `accessPolicy.mode=derived`:
   `backend/internal/controlplane/packspec/standard_objects_spec.json:737`
   `backend/internal/controlplane/packspec/standard_objects_spec.json:744`
   `backend/internal/controlplane/packspec/standard_objects_spec.json:1187`
   `backend/internal/controlplane/packspec/standard_objects_spec.json:1194`

This document therefore does not propose a brand-new architecture from
scratch. It explains the abstraction that best matches the direction already
present in the repository.

## One Gate, One Owner, One Reader

The practical rule is:

> for a given object and gate, choose one ownership model and make every layer
> read that same contract.

That rule can be expressed like this:

| Gate | Question | Ownership examples | Metadata source | Reader family |
| --- | --- | --- | --- | --- |
| Sharing | can the record be seen / queried? | `parent-derived`, own OWD/sharing | `SharingModel`, relationship metadata, sharing metadata | record access, RLS |
| Object CRUD | can the object be read/create/edit/delete? | `self-owned`, `composite-derived`, `delegated-derived` | `ObjectPermissions` or `AccessPolicy` | permission service object gate |
| Field FLS | can the field be read/edit? | `self-owned`, inherited-from-object-derived | `FieldPermissions` or `AccessPolicy` inherit terms | permission service field gate |

This is why the current design does not say:

- "all access should be unified into one parent walk"

It says:

- "for each gate, choose one owner model and make every layer read the same
  owner contract"

## The Ownership Modes

### 1. `self-owned`

The access surface is authored through the object's own effective:

- `ObjectPermissions`
- `FieldPermissions`

This remains the default generic model for many objects.

### 2. `parent-derived`

The access surface derives from one sharing parent or canonical parent
relationship.

This is the natural shape for:

- row-level visibility on `ControlledByParent` objects
- any future object whose CRUD/FLS owner is explicitly proven to be fully
  parent-derived

Important boundary:

- this mode should be used when the owner fact really is "the parent owns this
  gate"
- it should not be stretched to cover composite or actor-sensitive shapes just
  for the sake of uniformity

### 3. `composite-derived`

The access surface is a composition over other canonical permission facts.

Example:

- `OpportunityContactRole`

This is not a platform exception; it is a first-class ownership mode.

### 4. `delegated-derived`

The access surface derives from canonical permissions plus delegated or
actor-sensitive predicates.

Example:

- `CaseComment`

This is also a first-class ownership mode, not a special branch.

## Where Parent-Only Breaks Down

The repository work after `#1970` exposed three distinct object shapes.

### 1. Row-level-proven children

Examples:

- `QuoteLineItem`
- `OpportunityLineItem`
- `OrderItem`
- `PricebookEntry`

For this bucket, `ControlledByParent` is enough to prove the row-level
visibility baseline.

What it does **not** automatically prove is that the same object's CRUD/FLS
owner must also collapse into pure parent-derived ownership.

That is why the current design line keeps these facts separate:

- row-level behavior can be proven and frozen
- the strict authoring surface can remain preserved
- any stronger CRUD/FLS owner verdict still needs its own evidence package

This document intentionally does **not** promote that bucket to a stronger
final CRUD/FLS owner conclusion.

### 2. Composite-derived objects

Example:

- `OpportunityContactRole`

Salesforce's documented product surface for OCR is composite:

- read OCR: `Opportunity.Read AND Contact.Read`
- create/edit/delete OCR: `Opportunity.Edit AND Contact.Read`

Reference:

- Salesforce Help — Opportunity Contact Roles:
  <https://help.salesforce.com/s/articleView?id=sales.sales_core_opp_contact_add_manage.htm&language=en_US&type=5>

That is why the platform introduced a composite `AccessPolicy` shape rather
than pretending OCR is a generic self-owned or pure parent-only child.

### 3. Delegated-derived objects

Example:

- `CaseComment`

`CaseComment` also cannot be expressed as a pure parent-only child.

At the abstraction level of this document, the important fact is that its
access surface depends on more than a single parent-derived rule. The detailed
product-boundary contract remains in the dedicated CaseComment design.

The shape that matters here is:

- access to `Case`
- a case-comment-specific system permission
- whether the acting user authored the comment being edited/deleted

That means the platform needs an actor-sensitive delegated model, not a plain
single-parent derivation.

This overview doc does **not** attempt to restate every API/UI nuance for
CaseComment edit/delete behavior. That finer-grained product boundary belongs
in:

- `docs/design/permission-system/casecomment-delegated-access-policy.md`

References:

- Salesforce 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=sf.case_comments.htm&type=5>

## Why `AccessPolicy` Is the Right Platform Primitive

Once the platform recognizes that non-self ownership must exist, it needs one
metadata-authored primitive that can express those shapes generically.

That primitive is `AccessPolicy`.

`AccessPolicy` is the right fit because it preserves the project rules in
`CLAUDE.md`:

1. one fact, one owner
   - the non-self ownership fact lives on object metadata, not in scattered
     feature branches
2. one read family
   - runtime, seeder, repair, and verify can all key off the same ownership
     contract
3. blind interpreter
   - platform code evaluates generic policy expressions instead of branching on
     object API names
4. exhaust reuse before inventing
   - the platform reuses Salesforce primitives where they are sufficient
   - when they are not sufficient, it extends the metadata layer with one
     generic capability rather than adding object-specific runtime logic

So `AccessPolicy` should not be described as an OCR/CaseComment special case.
It is the generic metadata mechanism for **non-self-owned object/field access
surfaces**.

## Declared Delta From Salesforce

The declared deltas in this overview document are limited to the current
repository behavior this doc is explicitly describing. This is still not a
full restatement of every object-specific product-boundary nuance.

Salesforce does not expose a public field named `_System_Object.AccessPolicy`.
NexusCRM uses metadata-authored `AccessPolicy` expressions as the platform's
generic storage/evaluation mechanism for non-self-owned object/field access.

What is **not** a declared delta here:

- inventing object-name runtime rules for OCR / CaseComment
- silently claiming parent-only CRUD/FLS truth for objects that have not been
  proven that way

Object-specific product-boundary deltas, if any, must still be declared and
justified in the dedicated object-level design docs rather than being inferred
from this overview.

What **is** the declared delta:

- store non-self ownership as metadata-authored policy expressions so the
  engine can remain a generic interpreter

## Next-Tenant Impact

The next tenant should inherit:

1. the same Salesforce-shaped row-level primitive (`ControlledByParent`) for
   sharing inheritance
2. the same ownership-model vocabulary for explaining who owns object/field
   access
3. the same metadata-authored `AccessPolicy` substrate for composite or
   delegated object surfaces
4. no need for a tenant-specific code branch when another standard object turns
   out not to be purely self-owned

This is the main platform value of the abstraction: the next non-self-owned
standard object should fit into a known ownership mode rather than forcing a
new L0 special case.

This section is about the reusable ownership vocabulary and metadata
substrate. It is not attempting to restate every current row-level reader
detail as a future-tenant contract.

## Relationship to `ControlledByParent`

These concepts are complementary, not competing:

- `ControlledByParent` explains row-level inheritance
- `AccessPolicy` explains non-self ownership of object/field access when the
  surface cannot be faithfully modeled as self-owned permissions

In practice, that means:

- row-level visibility may still be parent-derived through
  `ControlledByParent`
- object/field ownership may simultaneously be composite-derived or
  delegated-derived through `AccessPolicy`

That is the current platform shape for:

- `OpportunityContactRole`
- `CaseComment`

So the newer design is not a rejection of `#1970`.
It is an abstraction upgrade from:

- "unify around one parent-derived rule"

to:

- "unify around one metadata-driven ownership-model framework"

## Current Repository Interpretation

The repository's current direction can be summarized as:

1. keep `ControlledByParent` as the row-level primitive
2. keep row-level-proven line-item objects on a narrow frozen baseline rather
   than over-claiming CRUD/FLS truth
3. model `OpportunityContactRole` through composite-derived ownership
4. model `CaseComment` through delegated-derived ownership
5. require runtime, seeder, repair, verify, and cleanup to align to the same
   ownership model for each object

This is why the newer work is best understood as an **access ownership model**
direction rather than a pure `ControlledByParent` cleanup.

## Verification

The current repository direction described by this document is mechanically
checkable in three places:

1. metadata / pack guardrails
   - OCR and CaseComment standard metadata shape:
     `backend/internal/controlplane/packs/platform_core_test.go:1074`
     `backend/internal/controlplane/packs/platform_core_test.go:1012`
2. runtime evaluation tests
   - `ControlledByParent` parent-filter wiring remains mechanically covered:
     `backend/internal/features/permissions/rls_test.go:338`
   - OCR derived access policy:
     `backend/internal/features/permissions/controlled_by_parent_standard_objects_test.go:306`
   - CaseComment delegated access policy:
     `backend/internal/features/permissions/controlled_by_parent_standard_objects_test.go:402`
   - actor-sensitive delegated predicates:
     `backend/internal/features/permissions/access_policy_test.go:336`
3. seeder behavior
   - derived objects skip self-row permission seeding:
     `backend/internal/features/permissions/access_policy_test.go:328`

These checks are sufficient for this document's narrower claim:

- `ControlledByParent` remains the row-level primitive for the abstraction this
  overview is describing
- non-self object/field ownership is already represented through
  metadata-authored `AccessPolicy`
- runtime and secondary layers already recognize that split

This document does **not** claim that every future ownership verdict is fully
closed. More specific object-level proof and rollout evidence still belong in
the dedicated OCR / CaseComment / profile convergence docs.

## Why This Is a Better Platform Story

A platform that unifies too low in the stack gets a cleaner short-term code
path at the cost of a less truthful product model.

For example:

- forcing every special child into `parent-derived`
- or preserving every object as if self-owned CRUD/FLS were always the truth

would each produce a superficially simple system, but one that encodes the
wrong owner fact for some objects.

The stronger platform story is:

- a small number of principled ownership modes
- one generic metadata mechanism for non-self modes
- no object-name branching in L0
- no silent collapse of product distinctions that matter to admins and future
  tenants

That gives the platform:

1. extensibility
   - new objects fit into known ownership modes instead of forcing new runtime
     special cases
2. explainability
   - contributors can reason about access through one vocabulary
3. portability
   - the platform stays metadata-driven and tenant-agnostic
4. operational consistency
   - runtime, seeder, repair, verify, and cleanup can converge on the same
     owner fact

The competitive target is therefore not:

- "everything is parent-derived"

It is:

- "every access surface belongs to a small, principled ownership algebra"

## Final Position

The right summary of the design evolution is:

1. `#1970` correctly pushed the platform away from registries and
   object-specific access logic
2. later design work showed that a pure parent-only model was too narrow for
   the full object set
3. the platform therefore generalized the unification target into an
   **access ownership model**
4. `AccessPolicy` is the metadata-authored mechanism for the non-self ownership
   modes in that model

In short:

> the platform did not abandon the `#1970` unification goal; it raised the
> abstraction from "one parent-derived rule" to "one metadata-driven ownership
> model framework."
