Apex Defaults to User Mode in Summer '26: What Actually Changes

By John Harris · 2026-07-11

For almost two decades, Apex has operated on a simple assumption: your code sees everything. Object permissions, field-level security, sharing rules - none of it applied unless you, the developer, deliberately opted in with WITH SECURITY_ENFORCED, Security.stripInaccessible(), sharing keywords, or an AccessLevel parameter. Opt-in security has an obvious flaw: it only works if every developer on every team remembers to opt in, every single time. In a large org, that's wishful thinking.

Salesforce Summer '26 ends that era. With API version 67.0, Apex becomes secure by default. Database operations now run in user mode, classes without a sharing declaration default to with sharing, and WITH SECURITY_ENFORCED is gone entirely. It's the most consequential change to the Apex security model in years - and if you bump a class to v67 without understanding it, you will see code behave differently in ways that look like bugs but are actually the platform working as newly designed.

This guide covers what changed, why, how user mode really differs from system mode, the trigger subtlety almost everyone gets wrong, and a practical migration plan.

What Changed in Summer '26?

Four things ship with this release. Three of them are tied to Apex API 67; one applies everywhere.

1. Database operations default to user mode (API v67+)

When a class is compiled at API version 67.0 or later, SOQL, SOSL, DML statements, and Database/Search methods run in user mode by default. They enforce the running user's object permissions (CRUD), field-level security (FLS), and sharing rules - automatically, with no extra syntax. On v66 and earlier, the same statements run in system mode, exactly as they always have.

2. Classes default to with sharing (API v67+)

Previously, a class with no sharing keyword didn't enforce sharing rules when it was the entry point - the practical effect was without sharing. At v67+, an omitted declaration now means with sharing. If you genuinely need unrestricted record visibility, you must declare without sharing explicitly.

3. WITH SECURITY_ENFORCED is removed (API v67+)

This is a hard, compiler-level break - not a deprecation warning. A class or trigger saved at v67 that contains WITH SECURITY_ENFORCED fails to compile. Code on older API versions keeps compiling, but the migration path is clear: replace it with WITH USER_MODE, which checks more (details below).

4. Triggers always run in system mode - on every API version

This one is versionless. Historically there were edge cases - notably around nested trigger execution - where sharing rules leaked into trigger context. Salesforce has closed those inconsistencies: all Apex triggers now execute in system mode, on all API versions, and sharing or access-mode declarations on triggers aren't permitted.

The one line to remember: triggers fire in system mode, but the SOQL and DML written inside v67+ code - including trigger bodies and handlers - default to user mode. These are two different things, and conflating them is the number-one misunderstanding of this release. More on this below.

Why did Salesforce do this?

Because opt-in security demonstrably doesn't scale. Every Apex security review, every stripInaccessible() retrofit, every FLS bug in an AppExchange submission traces back to the same root cause: the platform assumed elevated access and asked developers to remember to remove it. Flipping the default means a hotfix written at 2 a.m. by a tired developer is now safe unless someone deliberately escalates - and an explicit SYSTEM_MODE in a pull request is easy to spot and question in review. It also aligns Apex with how Flow and the standard UI have always behaved, and it makes security intent legible to Salesforce's security review team for managed packages.

Before vs After

API v66 and earlier → API v67+ (Summer '26):

  • SOQL / SOSL: system mode → user mode

  • DML statements: system mode → user mode

  • Database.* / Search.* methods: system mode → user mode

  • Class with no sharing keyword: sharing not enforced at entry point → with sharing

  • WITH SECURITY_ENFORCED: compiles (legacy) → removed - compile error

  • Trigger execution context: system mode (with rare sharing edge cases) → system mode, now consistent on all versions

Crucially, all of the version-specific behaviour keys off the compiled API version of each individual class or trigger. Upgrading your org to Summer '26 changes nothing by itself. A v58 utility class keeps behaving like a v58 utility class until you bump it. That's also why a single innocent-looking <apiVersion>67.0</apiVersion> in a meta XML file can change how every query in that class treats the running user.

User Mode vs System Mode

Understanding the user mode vs system mode distinction precisely is what separates a smooth migration from weeks of confusing tickets.

It all comes down to one question: whose permissions is the database operation evaluated against?

System mode trusts your code completely. Queries and DML ignore the running user's object permissions and field-level security - the code sees every record and every field it asks for. The only check that can apply is record sharing, and only when the class is declared with sharing.

User mode evaluates every operation against the actual running user. All three layers apply automatically - object permissions (can this user work with Accounts at all?), field-level security (are they allowed to see AnnualRevenue?), and sharing rules (which specific records can they touch?) - with no keywords or extra code required. In short: the same visibility the user gets in the standard UI, now enforced in your Apex.

The failure modes matter as much as the checks:

  • Sharing violations don't throw. A query simply returns fewer rows. A report of "the batch job skipped records" or "the component shows an empty list for some users" is the classic symptom.

  • CRUD and FLS violations throw. SOQL raises a System.QueryException; DML raises a DmlException. Unlike the old WITH SECURITY_ENFORCED, which failed on the first inaccessible field and told you nothing else, WITH USER_MODE reports the complete set of violations - dramatically better for debugging.

  • SOSL and DML follow the same rules. Search.query() and every DML verb (insert, update, upsert, delete, undelete, merge) respect the mode, and Database methods with allOrNone=false return per-record failures instead of throwing.

This is also why WITH USER_MODE isn't just a rename of WITH SECURITY_ENFORCED. The old clause only inspected fields in the SELECT and FROM clauses - a WHERE filter on a field the user couldn't read sailed straight through, quietly leaking information through filter logic. It also never touched sharing and choked on polymorphic relationships. User mode closes all three gaps.

Code Examples

The same query, two different behaviours

// Compiled at API v66 or earlier → SYSTEM MODE
// Returns every matching account, all fields, ignoring CRUD/FLS/sharing.
//
// Compiled at API v67+ → USER MODE
// Returns only records the running user can see; throws
// System.QueryException if they lack access to an object or field.
List<Account> accts = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
    WHERE Rating = 'Hot'
];

Be explicit - in both directions

// Explicit user mode (recommended even at v67+, where it's the default)
List<Account> visible = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
    WHERE Rating = 'Hot'
    WITH USER_MODE
    ORDER BY Name
];

// Explicit system mode - a deliberate, reviewable escalation
List<Account> everything = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
    WHERE Rating = 'Hot'
    WITH SYSTEM_MODE
];

DML

// User mode DML - enforces CRUD, FLS, and sharing for the running user
insert as user newContacts;

// System mode DML - explicit escalation for internal bookkeeping
update as system integrationLogs;

Database and Search methods

// Partial-success DML in user mode
Database.SaveResult[] results =
    Database.insert(newContacts, false, AccessLevel.USER_MODE);

// Dynamic SOQL with an explicit access level
List<Account> rows = Database.query(
    'SELECT Id, Name FROM Account WHERE Rating = :rating',
    AccessLevel.SYSTEM_MODE
);

// SOSL in user mode
List<List<SObject>> found =
    Search.query(soslString, AccessLevel.USER_MODE);

Migrating off WITH SECURITY_ENFORCED

// BEFORE - fails to compile at API v67+
List<Contact> cons = [SELECT Id, Email FROM Contact WITH SECURITY_ENFORCED];

// AFTER - checks CRUD + FLS + sharing, covers the WHERE clause,
// supports polymorphic fields, reports every violation at once
List<Contact> cons = [SELECT Id, Email FROM Contact WITH USER_MODE];

Good vs bad patterns

// BAD: nuking the release to "make the errors go away".
// This silently restores pre-v67 behaviour everywhere and defeats
// the entire point of secure-by-default Apex.
public without sharing class AccountService {
    public static List<Account> fetch() {
        return [SELECT Id, Name FROM Account WITH SYSTEM_MODE];
    }
}
// GOOD: user mode at the boundary, system mode surgically,
// wrapped and documented so the escalation is visible in review.
public with sharing class AccountService {

    @AuraEnabled(cacheable=true)
    public static List<Account> getMyAccounts() {
        return [SELECT Id, Name FROM Account WITH USER_MODE ORDER BY Name];
    }

    // System mode on purpose: sales users can't edit these internal
    // scoring fields, but the platform must still maintain them.
    private static void stampScore(List<Account> accts) {
        update as system accts;
    }
}

Trigger Behaviour: The Subtlety Everyone Will Get Wrong

Read this section twice, because the two halves sound contradictory and aren't.

The trigger itself executes in system mode. When a DML operation fires a trigger, the trigger runs regardless of the invoking user's broader permissions, and - as of Summer '26 - this is now consistent on every API version, with the old sharing edge cases removed. You cannot put a sharing declaration on a trigger, and you never could opt a trigger's execution context into user mode.

But the database operations written inside v67+ code default to user mode. The mode of each SOQL query or DML statement is determined by the API version of the file it lives in. Save a trigger - or the handler class it delegates to - at v67, and every unmarked query and DML statement in it now enforces the running user's permissions.

Here's why that combination is dangerous. Consider the classic rollup pattern:

// OpportunityTrigger.trigger - API v67
trigger OpportunityTrigger on Opportunity (after insert, after update) {
    OpportunityRollupHandler.recalculate(Trigger.new);
}
// OpportunityRollupHandler.cls - API v67
public with sharing class OpportunityRollupHandler {

    public static void recalculate(List<Opportunity> opps) {
        Set<Id> acctIds = new Set<Id>();
        for (Opportunity opp : opps) {
            if (opp.AccountId != null) acctIds.add(opp.AccountId);
        }

        // At v67 these default to USER MODE. A sales rep who can edit
        // Opportunities but not Account.Open_Pipeline__c now makes this
        // trigger throw - or worse, the query silently misses accounts
        // outside their sharing scope and rollups quietly drift.
        List<Account> accts = [
            SELECT Id, Open_Pipeline__c FROM Account WHERE Id IN :acctIds
        ];
        // ... recalc ...
        update accts;
    }
}

The fix is not to abandon user mode - it's to escalate only the internal bookkeeping:

        // System mode on purpose: rollup maintenance is platform
        // logic, not a user action. Users don't need - and shouldn't
        // need - edit access to internal rollup fields.
        List<Account> accts = [
            SELECT Id, Open_Pipeline__c
            FROM Account
            WHERE Id IN :acctIds
            WITH SYSTEM_MODE
        ];
        // ... recalc ...
        update as system accts;

Rule of thumb for triggers and handlers:

  • Keep user mode for anything that expresses the user's intent on data they work with - validating their input, creating child records they logically own, querying data to show back to them.

  • Use explicit system mode for cross-object side effects the platform must guarantee regardless of who triggered them - rollups, audit stamps, integration sync fields, denormalised counters. Add a one-line comment justifying each escalation.

Watch your automation contexts. Asynchronous Apex (Queueable, Batch, @future) evaluates user mode against the user who enqueued the job. Platform event triggers, however, run as the Automated Process user by default - user-mode operations there will fail unless that user has access, you configure a run-as user via PlatformEventSubscriberConfig, or you deliberately use system mode.

Migration Guide

The good news: nothing breaks on upgrade day, because everything is versioned per file. The bad news: that means the risk transfers to every future API version bump, including the automatic ones your deployment tooling or IDE might make. Treat the migration as a project, not an event.

1. Inventory your exposure. Three 'greps' against your repo give you the backlog: classes with no sharing declaration, occurrences of WITH SECURITY_ENFORCED, and bare SOQL/DML/Database calls with no explicit access level. Salesforce Code Analyser and PMD rules can automate this in CI.

2. Fix the compile break first. Every WITH SECURITY_ENFORCED must become WITH USER_MODE before the file can ever be saved at v67. Note that this is a behaviour upgrade, not a find-and-replace equivalence - the new clause also enforces sharing and checks WHERE clauses, so test it.

3. Declare sharing on every class. Don't let the new default decide for you. An explicit with sharing, without sharing, or inherited sharing on every class makes behaviour version-independent and reviewable.

4. Add explicit access levels everywhere. WITH USER_MODE, as user, AccessLevel.USER_MODE - even where it's now the default. Explicit code survives being copy-pasted into an older-version file, and it makes the rare SYSTEM_MODE stand out in review.

5. Bump versions incrementally. Prioritise entry points - @AuraEnabled controllers, @InvocableMethod classes, REST resources, batch classes - because those are where the sharing default flip bites hardest. Move one low-risk class to v67 in a sandbox, test, expand.

6. Regression-test as real personas, not as admin. Tests run as System Administrator prove nothing about user mode. Use System.runAs() with profiles and permission sets that mirror your actual user base. A quick diagnostic when a query returns fewer rows than expected:

Integer userRows   = [SELECT COUNT() FROM Account WITH USER_MODE];
Integer systemRows = [SELECT COUNT() FROM Account WITH SYSTEM_MODE];
System.debug('User: ' + userRows + ' / System: ' + systemRows);

A large gap confirms the drop is the user-mode default doing its job, not missing data.

7. Audit permission sets. Many "failures" after the bump are really permission gaps that system mode was papering over for years. Sometimes the right fix is granting the access users should have had all along.

8. Expect a positive security review. For ISVs and managed packages, explicit access levels make your security posture legible - fewer implicit behaviours means fewer questions from the review team.

Best Practices

  • Declare a sharing keyword on every class - never rely on defaults, old or new.

  • State the access mode on every database operation, even where user mode is now implicit.

  • Treat SYSTEM_MODE and without sharing as escalations: wrap them in narrowly-named private methods and comment the justification.

  • Enforce user mode at trust boundaries (UI controllers, REST endpoints, invocable actions); reserve system mode for internal platform bookkeeping.

  • Prefer Security.stripInaccessible() when gracefully hiding fields is better UX than throwing.

  • Run persona-based tests with System.runAs() in CI; add a full-org test run before each release window.

  • Lint for undeclared sharing and bare database calls with Salesforce Code Analyser so new debt can't accumulate.

  • Never blanket-pin API versions to v66 to avoid the work - you're only deferring it with interest.

Common Pitfalls

Assuming triggers now run in user mode. They don't. The trigger context is system mode everywhere, forever; only the database operations inside v67+ code flipped. Developers will misdiagnose both directions of this.

Assuming the org upgrade changed behaviour. It didn't. Behaviour changes per class, per API version bump. The flip side: a deployment tool that silently bumps apiVersion can change production behaviour in a "no-code-change" release.

Tests that pass because they run as admin. The default test context has full access, so user-mode enforcement is invisible until a real user hits it. System.runAs() is no longer optional.

Silent row filtering mistaken for data loss. Sharing violations don't throw - queries just return less. Batch jobs "skipping" records and components rendering empty lists for certain users are the signature symptoms.

Mixed-version call stacks. A v67 controller calling a v58 utility gets user mode in the controller and system mode in the utility. Until everything is explicit, enforcement is inconsistent in ways that are miserable to debug.

Forgetting automated contexts. Platform event triggers running as Automated Process, integration users with minimal profiles, portal/guest users - anywhere the "running user" is unusual, user-mode defaults surface permission gaps fast.

Blanket system mode. Converting every failure to WITH SYSTEM_MODE restores the old behaviour and fails the next security review. Fix permissions or escalate surgically.

Final Thoughts

Summer '26 is Salesforce closing a gap that has existed since Apex shipped: security you had to remember versus security you get by default. For Salesforce developers, the mechanics are simple - user mode by default, with sharing by default, WITH SECURITY_ENFORCED gone, triggers consistently system mode - but the mindset shift is the real change. System-level access is now a decision you make out loud, in code, where your reviewers can see it.

Start this week: run the inventory greps, replace WITH SECURITY_ENFORCED, declare sharing everywhere, and bump one sandbox class to API v67 with persona tests watching. The orgs that hurt in Summer '26 will be the ones that let the version bump make these decisions for them. Make them yourself instead.

Always validate behavior against the official Summer '26 release notes and a preview sandbox before migrating production code.

What this means if you're studying for Platform Developer

The Platform Developer exam guide says its questions align to the Summer '25 release. That's before API version 67.0, so the exam still expects the old defaults: system mode unless you ask for user mode, and WITH SECURITY_ENFORCED as a valid clause. Learn both models, and answer exam questions by the release the guide names. Our Platform Developer practice course follows the current guide, with every option explained.

Sources

More articles