Salesforce

Apex Sharing in Salesforce: Manual Share, With Sharing, Without Sharing

Salesforce enforces record-level security through a layered sharing model. Apex code runs in system context by default, meaning it can bypass those layers entirely — which is powerful but dangerous. Understanding with sharing, without sharing, inherited sharing, and programmatic (manual) sharing lets you write code that is both functional and secure. This session covers all four mechanisms with working code you can deploy and test.

How Salesforce Record Security Works

Before writing a single line of Apex, you must understand the hierarchy Salesforce uses to determine whether a user can see or edit a record:

  1. Organization-Wide Defaults (OWD): The baseline. If OWD is Private, users see only their own records.
  2. Role Hierarchy: Users above a record owner in the hierarchy get access if "Grant Access Using Hierarchies" is enabled.
  3. Sharing Rules: Declarative rules that extend access to groups or roles based on record criteria or ownership.
  4. Manual Sharing: Record owners (or admins) share individual records with specific users or groups.
  5. Apex Managed Sharing: Programmatic sharing you write in Apex, stored in share objects, identified by a custom rowCause.

Apex code ignores all of these by default unless you explicitly opt in. That opt-in is what with sharing provides.

The Three Sharing Keywords

with sharing

Declaring a class with sharing tells Salesforce to enforce the running user's sharing rules when that class executes SOQL queries, DML operations, and record access checks. If the user cannot see a record under normal Salesforce security, they cannot see it through this class.

public with sharing class AccountService {

    public List<Account> getMyAccounts() {
        // Only returns accounts the running user has access to.
        // If OWD is Private and the user owns 3 accounts, this returns 3 rows.
        return [SELECT Id, Name, Industry FROM Account ORDER BY Name];
    }

    public void updateAccountRating(Id accountId, String rating) {
        // DML also respects sharing. If the user has read-only access,
        // this throws a DmlException: "insufficient access rights on object id".
        Account acc = new Account(Id = accountId, Rating = rating);
        update acc;
    }
}

Use with sharing on every class that handles user-facing operations — Apex controllers for LWC and Aura, service classes called from flows, REST endpoints exposed to end users. This is the correct default for most business logic.

without sharing

Declaring a class without sharing runs all code in system context, ignoring the running user's sharing rules entirely. The class can read, insert, update, and delete any record in the org regardless of who is running it.

public without sharing class AuditLogService {

    // This runs in system context. It logs every account change
    // regardless of whether the invoking user can see those accounts.
    public static void logChange(Id recordId, String action) {
        Audit_Log__c log = new Audit_Log__c(
            Record_Id__c = recordId,
            Action__c    = action,
            Logged_At__c = Datetime.now(),
            User__c      = UserInfo.getUserId()
        );
        insert log;
    }
}

Legitimate use cases: background jobs, scheduled batches that process all records in the org, internal platform utilities that must read records across ownership boundaries (such as duplicate detection or data migration). Never expose without sharing classes directly to user input without explicit authorization checks you write yourself.

Critical point: When a with sharing class calls a method on a without sharing class, the called class runs in system context. The sharing boundary lives at the class level, not the call stack.

inherited sharing

inherited sharing was introduced in API version 42.0. A class declared with this keyword adopts the sharing context of its caller. If called from a with sharing context, sharing is enforced. If called from a without sharing context, it is not.

public inherited sharing class RecordRepository {

    // This class is a shared utility. It should respect whatever
    // sharing context the caller established.
    public List<Contact> getContactsByAccount(Id accountId) {
        return [
            SELECT Id, FirstName, LastName, Email
            FROM Contact
            WHERE AccountId = :accountId
            ORDER BY LastName
        ];
    }
}

If you omit the sharing keyword entirely (which is not the same as inherited sharing), Apex runs in system context by default — equivalent to without sharing. This is a common source of unintended data exposure. Always declare a sharing keyword explicitly.

Comparing the Three Keywords

  • with sharing — enforces running user's sharing. Default choice for user-facing logic.
  • without sharing — bypasses sharing completely. Use only for platform utilities, background automation, or when you've implemented your own authorization layer.
  • inherited sharing — delegates the decision to the caller. Best for reusable service or repository classes used in both contexts.
  • No keyword declared — behaves like without sharing but is implicit and easy to miss in code reviews. Avoid.

Calling Across Sharing Boundaries

A common architectural pattern is to run the outer controller with sharing and push privileged operations into an inner class declared without sharing. This keeps the surface area of elevated privilege small and auditable.

public with sharing class CaseController {

    // Runs with sharing — the user sees only their cases
    public static List<Case> getUserCases() {
        return [SELECT Id, Subject, Status FROM Case ORDER BY CreatedDate DESC];
    }

    // To escalate a case, we need to update a record the user
    // might not own. We delegate only that operation to a
    // tightly scoped without sharing class.
    public static void escalateCase(Id caseId) {
        CaseEscalator.runEscalation(caseId);
    }

    // Inner class: elevated privilege, narrow responsibility
    private without sharing class CaseEscalator {
        static void runEscalation(Id caseId) {
            Case c = [SELECT Id, Priority FROM Case WHERE Id = :caseId LIMIT 1];
            c.Priority = 'High';
            c.Status   = 'Escalated';
            update c;
        }
    }
}

Notice the inner class pattern. The without sharing block touches only the fields required for escalation. It does no SOQL that returns data to the user, so there is no risk of leaking records the user shouldn't see.

Apex Managed Sharing (Manual Share in Code)

When declarative sharing rules are insufficient — for example, when you need to share a record based on complex business logic evaluated at runtime — you use Apex Managed Sharing. Every standard and custom object has a corresponding share object. For a custom object Project__c, the share object is Project__Share. For Account, it is AccountShare.

Share Object Fields

  • ParentId — the ID of the record being shared.
  • UserOrGroupId — the ID of the User, Role, or Public Group receiving access.
  • AccessLevel'Read', 'Edit', or 'All' (All = full control, only available for some objects).
  • RowCause — identifies why the share record exists. For Apex Managed Sharing you must use a custom Share Reason (defined on the object) with the __c suffix. The system cause is Manual.

Creating a Custom Share Reason

In Setup, navigate to the custom object → Apex Sharing Reasons → New. Create a reason named Project_Team_Member. Salesforce stores it as Project_Team_Member__c. You reference it in Apex as the string 'Project_Team_Member__c'.

Sharing a Custom Object Record

public without sharing class ProjectSharingService {

    // Grant edit access to a user on a project record.
    // This must run without sharing because we are creating
    // share records that require elevated permission.
    public static void shareProjectWithUser(Id projectId, Id userId) {
        Project__Share share = new Project__Share();
        share.ParentId       = projectId;
        share.UserOrGroupId  = userId;
        share.AccessLevel    = 'Edit';
        share.RowCause       = 'Project_Team_Member__c';

        try {
            insert share;
        } catch (DmlException e) {
            // Duplicate share records throw DUPLICATE_VALUE.
            // Decide whether to surface this as a user error or swallow it.
            if (!e.getMessage().contains('DUPLICATE_VALUE')) {
                throw e;
            }
        }
    }

    // Revoke access when a user is removed from the project team.
    public static void revokeProjectAccess(Id projectId, Id userId) {
        List<Project__Share> toDelete = [
            SELECT Id
            FROM Project__Share
            WHERE ParentId      = :projectId
            AND   UserOrGroupId = :userId
            AND   RowCause      = 'Project_Team_Member__c'
        ];
        if (!toDelete.isEmpty()) {
            delete toDelete;
        }
    }
}

Sharing a Standard Object Record

public without sharing class AccountSharingService {

    // Share an account with a public group for read access.
    // Useful when a support team needs visibility on specific accounts
    // without being in the owner's role hierarchy.
    public static void shareAccountWithGroup(Id accountId, Id groupId) {
        AccountShare share   = new AccountShare();
        share.AccountId      = accountId;   // Note: AccountShare uses AccountId, not ParentId
        share.UserOrGroupId  = groupId;
        share.AccountAccessLevel       = 'Read';
        share.OpportunityAccessLevel   = 'None'; // Required field on AccountShare
        share.CaseAccessLevel          = 'None'; // Required field on AccountShare
        share.RowCause                 = 'Manual';

        try {
            insert share;
        } catch (DmlException e) {
            if (!e.getMessage().contains('DUPLICATE_VALUE')) {
                throw e;
            }
        }
    }
}

AccountShare requires you to specify access levels for related Opportunity and Case records. This is unique to Account — other objects use a single AccessLevel field.

Bulk Apex Managed Sharing

In triggers and batch jobs, always build a list and perform a single DML call. Never insert share records one by one inside a loop.

public without sharing class BulkProjectSharingService {

    // Called from an after-insert trigger on Project_Member__c.
    // Project_Member__c is a junction object: Project__c + User__c.
    public static void applySharing(List<Project_Member__c> newMembers) {
        List<Project__Share> sharesToInsert = new List<Project__Share>();

        for (Project_Member__c member : newMembers) {
            Project__Share share = new Project__Share();
            share.ParentId      = member.Project__c;
            share.UserOrGroupId = member.User__c;
            share.AccessLevel   = member.Is_Lead__c ? 'Edit' : 'Read';
            share.RowCause      = 'Project_Team_Member__c';
            sharesToInsert.add(share);
        }

        if (!sharesToInsert.isEmpty()) {
            // allOrNone = false: allows partial success.
            // Inspect results for errors instead of letting duplicates fail the batch.
            Database.SaveResult[] results = Database.insert(sharesToInsert, false);

            for (Database.SaveResult sr : results) {
                if (!sr.isSuccess()) {
                    for (Database.Error err : sr.getErrors()) {
                        if (err.getStatusCode() != StatusCode.DUPLICATE_VALUE) {
                            System.debug(LoggingLevel.ERROR,
                                'Share insert failed: ' + err.getMessage());
                        }
                    }
                }
            }
        }
    }
}

Recalculating Sharing with Database.recalculateSharing

When your sharing logic depends on field values and those values change — for example, a project status changes from Draft to Active and should now be visible to a broader group — you can trigger Salesforce to recalculate sharing for a set of records. This only recalculates criteria-based sharing rules, not Apex Managed Sharing rows.

public without sharing class ProjectStatusHandler {

    public static void onStatusChange(List<Project__c> updatedProjects) {
        List<Project__c> activatedProjects = new List<Project__c>();

        for (Project__c p : updatedProjects) {
            if (p.Status__c == 'Active') {
                activatedProjects.add(p);
            }
        }

        if (!activatedProjects.isEmpty()) {
            // Tells Salesforce to re-evaluate sharing rules for these records.
            // Criteria-based sharing rules that match Status__c = 'Active'
            // will now fire and add the appropriate share rows.
            Database.recalculateSharing(activatedProjects);
        }
    }
}

Testing Apex Sharing

Apex tests run in system context by default. To test sharing enforcement, you must create test users with the System.runAs() method and use @isTest(SeeAllData=false) so records created in the test are isolated.

@isTest
private class AccountServiceTest {

    @TestSetup
    static void setup() {
        // Create a standard user without elevated permissions
        Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
        User testUser = new User(
            FirstName          = 'Test',
            LastName           = 'SharingUser',
            Email              = '[email protected]',
            Username           = 'test.sharing.' + Datetime.now().getTime() + '@example.com',
            Alias              = 'tshare',
            TimeZoneSidKey     = 'America/New_York',
            LocaleSidKey       = 'en_US',
            EmailEncodingKey   = 'UTF-8',
            LanguageLocaleKey  = 'en_US',
            ProfileId          = p.Id
        );
        insert testUser;
    }

    @isTest
    static void testWithSharingEnforced() {
        User testUser = [SELECT Id FROM User WHERE Email = '[email protected]' LIMIT 1];

        // Insert an account owned by the test user
        Account ownedAccount = new Account(Name = 'Owned Account', OwnerId = testUser.Id);
        insert ownedAccount;

        // Insert an account owned by someone else (no sharing granted)
        Account otherAccount = new Account(Name = 'Other Account');
        insert otherAccount;

        System.runAs(testUser) {
            AccountService svc = new AccountService();
            List<Account> results = svc.getMyAccounts();

            // If OWD is Private, the test user should see only their own account
            System.assertEquals(1, results.size(),
                'User should see only records they own when OWD is Private');
            System.assertEquals(ownedAccount.Id, results[0].Id);
        }
    }
}

The test above only works as described when the Account OWD is Private. In orgs with Public Read/Write OWD, all accounts are visible to everyone and the assertion would need to change. Always document the OWD assumption in your test or check programmatically with Schema.getGlobalDescribe().

Security Review Checklist

  • Every class has an explicit sharing keyword — no implicit system context.
  • without sharing classes are small, focused, and contain no SOQL that returns data to the UI.
  • Apex Managed Sharing uses a custom RowCause, not Manual, so platform tools can track and recalculate it separately.
  • Bulk DML: share records inserted in a single list DML, never inside a loop.
  • Share records are deleted when the underlying business relationship ends.
  • Tests use System.runAs() to assert behavior from the perspective of a limited user.

Summary

Apex runs in system context by default, which means incorrectly written code can expose records to users who should never see them. Use with sharing as your default on every class that handles user-initiated operations. Reserve without sharing for tightly scoped platform utilities where you own the authorization logic. Use inherited sharing on reusable library classes that must adapt to their calling context. When declarative sharing rules cannot express your business requirements — complex team structures, dynamic membership, runtime-evaluated criteria — implement Apex Managed Sharing with a custom share reason, always performing bulk DML and cleaning up share rows when access should be revoked. These four mechanisms, used together deliberately, give you precise control over every record visibility decision in your org.

← All articles

Frequently Asked Questions

What happens if a class has no sharing keyword at all?

A class with no sharing keyword inherits the sharing context of whatever called it. If called from a with sharing class, it enforces sharing; if called from without sharing code or a scheduled job, it runs in system context. This makes the behavior unpredictable, which is why inherited sharing exists as an explicit declaration to make that inheritance intentional and readable.

When is it safe to use without sharing in production code?

It is appropriate when the operation must succeed regardless of who triggers it — audit logging, background cleanup jobs, or internal platform tasks where the running user's permissions are irrelevant to correctness. The key safeguard is to keep without sharing classes narrow in scope and never expose their methods directly to user-facing controllers, so system-level access cannot be exploited through user input.

How does Apex Managed Sharing differ from creating a manual share through the UI?

Manual sharing done through the UI is tied to the record owner and can be removed by that owner or an admin, while Apex Managed Sharing uses a custom rowCause stored in the share object that only Apex code or an admin can remove. This makes Apex Managed Sharing more durable and automation-friendly — it survives ownership changes and can encode business logic about why a specific user was granted access.

Does with sharing enforce field-level security (FLS) in addition to record access?

No — with sharing enforces record-level sharing rules only, not field-level security or object-level CRUD permissions. To enforce FLS in Apex, the code must explicitly check Schema.DescribeFieldResult or use the WITH SECURITY_ENFORCED clause in SOQL, or call the Security.stripInaccessible method before returning data to the user.