SfSkills Domains Docs GitHub

Home / security / apex-managed-sharing-patterns

apex-managed-sharing-patterns

Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design — use admin/sharing-and-visibility.

Open the skill folder on GitHub


Apex Managed Sharing Patterns

Apex managed sharing writes rows into an object's __Share table with a developer-defined Apex sharing reason, so the platform maintains those rows across owner changes instead of reclaiming them. It is the last mechanism in the record-access model, reached only when org-wide defaults, the role hierarchy, sharing rules, teams, and manual sharing cannot express the policy.

The one constraint that determines whether this skill applies at all:

"Apex sharing reasons and Apex managed sharing recalculation are only available for custom objects." — Apex Developer Guide, Understanding Sharing

If the record needing programmatic access is an Opportunity, Account, Case, or any other standard object, Apex managed sharing is not available. You can still insert OpportunityShare rows from Apex, but they are user managed (manual) shares with RowCause = 'Manual', and the platform deletes them the moment the record owner changes. Establish which side of that line you are on before designing anything.


Before Starting

Answer these four questions. Three of the four regularly kill the design.

  1. Is the target a custom object? If not, Apex managed sharing is off the

    table. Route to the built-in team feature (AccountTeamMember, OpportunityTeamMember, CaseTeamMember), to criteria-based sharing rules, or accept manual-share lifecycle and plan the re-grant on owner change.

  2. Is the object on the detail side of a master-detail relationship? Detail

    records have no __Share object at all; their access is derived from the master and the relationship's sharing setting. Changing that is a data-model migration, not a configuration change.

  3. What is the org-wide default? A share row must grant access more

    permissive than the OWD or the platform rejects it. On a Public Read/Write object no share row of any level is valid, and on Public Read Only a Read share is rejected.

  4. Can a declarative mechanism still do this? The Salesforce Security Guide

    allows "up to 300 total sharing rules for each object, including up to 50 criteria-based or guest user sharing rules." Teams routinely reach for code long before they are anywhere near that. Read 0 and cite the branch that sent you here.


Core Concepts

The share object

Every shareable object has a companion sObject. Standard objects use AccountShare, ContactShare, OpportunityShare, CaseShare, LeadShare. Custom objects use MyObject__Share. Every share row has four meaningful fields:

FieldNotes
ParentIdThe shared record's Id. Not updateable. On standard-object share sObjects this is named for the object, e.g. OpportunityId.
UserOrGroupIdTarget user, public group, role-derived sharing group, or territory group. Not updateable. Cannot be an unauthenticated guest user.
AccessLevelRead, Edit, or All. All is internal-only and cannot be granted. None exists on AccountShare only. On standard objects the field is named for the object, e.g. OpportunityAccessLevel.
RowCauseWhy access was granted. Determines who may alter the row. Not updateable.

RowCause is the whole point

RowCause is what separates a share the platform maintains from a share the platform throws away.

Sharing typeRowCause valueLifecycle
OwnerOwnerPlatform-maintained
Role hierarchy(derived at runtime, no row)Not stored
Sharing ruleRuleRebuilt on recalculation
Opportunity/Account/Case teamTeamPlatform-maintained
Territory ruleTerritoryRulePlatform-maintained
Implicit parent/childImplicitParent / ImplicitChildPlatform-maintained, not writable
Manual sharingManualDeleted when record owner changes
Apex managed sharingdeveloper-defined, e.g. Recruiter__cMaintained when record owner changes or is deactivated

Manual shares written from Apex default to RowCause = 'Manual', and the Apex Developer Guide notes that "Only shares with this condition are removed when ownership changes." That sentence is the entire business case for Apex managed sharing.

Apex sharing reasons

A sharing reason is metadata on the custom object with a label (shown in the Reason column of the record's sharing detail) and a name used in code. Names follow MyReasonName__c and are referenced as:

Schema.CustomObject__Share.RowCause.SharingReason__c
// e.g.
Schema.Job__Share.RowCause.Recruiter__c

Two operational facts about creating them:

Permission to write shares

Writing __Share rows requires Modify All Data — quoted from the Apex Developer Guide, with the consequences for a normal user's save path, in 1.

This is not the same thing as with sharing / without sharing, which control whether the running user's record access is enforced on queries and DML inside the class. At API 67.0 database operations default to user mode and a bare class defaults to with sharing; the correct construction is to leave the class with sharing and make only the __Share DML explicit:

Database.insert(shares, false, AccessLevel.SYSTEM_MODE);

Common Patterns

Pattern A — trigger-driven grant and revoke

The default. A service class exposes grantForRecords(List<SObject>) and revokeForRecords(Set<Id>); an after insert, after update trigger calls both. The revoke must filter on the application's own RowCause so it never deletes Owner, Rule, Team, or end-user Manual rows. Full worked implementation in 8, Example 1.

Pattern B — asynchronous grant for bulk paths

When the fan-out is wide (one record shared to many users) or the load is an ETL insert rather than an interactive save, move share DML into a Queueable or Batch keyed on record Ids. A 200-record trigger batch sharing to 300 users each is 60,000 rows in one transaction, which exceeds the 10,000-record DML limit. Accept the visible window in which the record exists and the access does not, and document it.

Pattern C — group sharing instead of user sharing

Share to a public group rather than to N users. One row per record per group instead of N rows. The trade is that group membership becomes the thing you maintain, and membership changes trigger their own asynchronous recalculation. Prefer this above roughly 50 users per record.

Pattern D — the recalculation class

A Database.Batchable class registered under Object Manager → [object] → Apex Sharing Recalculation (Classic only). The platform runs it for you:

Changing a custom object's org-wide default re-runs every Apex recalculation class registered against it — quoted and unpacked in 0.

and, from the Security Guide, "When sharing is recalculated, Salesforce also runs all Apex sharing recalculations." Without this class, an admin changing the OWD silently destroys your access model. Treat it as mandatory.


Decision Guidance

SituationMechanism
Access follows record ownershipNothing — the platform grants Full Access to the owner and the hierarchy above them
Access follows a field value, granted to a group or roleCriteria-based sharing rule
Access follows named users on a standard objectBuilt-in team (AccountTeamMember / OpportunityTeamMember / CaseTeamMember)
Access follows named users on a custom object, must survive owner changeApex managed sharing with a custom sharing reason
Access follows named users, may be reclaimed on owner changeManual share from Apex (RowCause = 'Manual') — plan the re-grant
Access must be removed from users who would otherwise have itRestriction rules, not sharing — sharing only grants
Target is an unauthenticated guest userGuest user sharing rule — Apex cannot share to guests

  1. Confirm the object is custom and its OWD is restrictive. Verify the target

    sObject name ends in __c, is not the detail side of a master-detail relationship, and has an org-wide default of Private (or Public Read Only when you only grant Edit). If any of these fail, stop and route to the decision guidance table above.

  2. Create the Apex sharing reason as metadata, one per distinct business

    reason for access, in objects/<Object>__c/sharingReasons/<Reason>.sharingReason-meta.xml. Deploy before the Apex that references it, or the class will not save.

  3. Write a service class with paired grant and revoke, both bulk-safe,

    both scoped by RowCause, with the share DML in AccessLevel.SYSTEM_MODE and Database.insert(..., false, ...) so one bad row does not abort the batch. Treat FIELD_FILTER_VALIDATION_EXCEPTION mentioning AccessLevel as expected.

  4. Invoke from an after insert, after update trigger, never before — the

    record has no Id in a before-insert context and ParentId is not updateable.

  5. Write the recalculation class implementing Database.Batchable<sObject>

    and register it under Apex Sharing Recalculation. Add the registration to the sandbox post-refresh checklist.

  6. Test with System.runAs, asserting both that the target user can query the

    record and that access disappears when the driving relationship is removed. A __Share row-count assertion proves nothing.


Review Checklist


Salesforce-Specific Gotchas

Full detail with quotes and reproduction conditions in 0.

  1. Apex sharing reasons do not exist on standard objects. The compile error on

    Schema.OpportunityShare.RowCause.Anything__c is the platform telling you the design is unavailable, not that a setting is missing.

  2. The sharing reason UI is Classic-only. Lightning Experience never renders

    the Apex Sharing Reasons or Apex Sharing Recalculation related lists.

  3. A share not more permissive than the OWD is rejected with

    FIELD_FILTER_VALIDATION_EXCEPTION. Relaxing an OWD converts every managed share in the org into an error.

  4. AccessLevel.USER_MODE on share DML works only for Modify All Data holders

    — that is, in your admin sandbox but not for real users.

  5. An OWD change wipes shares and re-runs registered Apex recalculation

    classes. No recalculation class means no rebuild.

  6. Unfiltered __Share deletes destroy other mechanisms' rows. Rule rows come

    back on recalculation; end-user manual shares do not.

  7. Enabling digital experiences widens RoleAndSubordinates grants to include

    portal subordinates automatically — an internal-only implementation becomes an external exposure with no code change.


Output Artifacts

ArtifactDescription
Sharing policy statementOne sentence per reason: who gets access, at what level, driven by which field or relationship, and when it is withdrawn
SharingReason metadataOne file per reason under objects/<Object>__c/sharingReasons/
Share service classBulk-safe grant / revoke pair with RowCause-scoped DML and documented system-mode escape
Recalculation class + registrationDatabase.Batchable implementation plus the Object Manager registration step recorded in the post-refresh checklist
Access test classSystem.runAs assertions covering grant, re-assignment, and revocation

Package files