XSCREENSAVER / 3D PIPES

[readonly] markdown buffer

Secure-by-default Apex in Summer '26

Apr 22, 2026 · 6 min read

Summer '26 moves API 67.0 Apex towards a safer baseline. Database operations default to user mode, classes without a sharing declaration enforce sharing, and WITH SECURITY_ENFORCED gives way to WITH USER_MODE.

The direction is consistent: code that needs elevated access should say so.

Four changes to review

1. User-facing reads become safer

An LWC controller should make both boundaries visible:

public with sharing class CaseSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<Case> recentCases() {
        return [
            SELECT Id, Subject
            FROM Case
            WITH USER_MODE
            LIMIT 20
        ];
    }
}

with sharing states the class's record-sharing posture. WITH USER_MODE applies the current user's object, field and record access to the query. Even where the new defaults produce the same result, explicit code is easier to review.

2. Writes need the same decision

Read security is only half the operation:

Opportunity opportunity = [
    SELECT Id, Rating__c
    FROM Opportunity
    WHERE Id = :opportunityId
    WITH USER_MODE
    LIMIT 1
];

opportunity.Rating__c = 'Hot';
update as user opportunity;

Dynamic SOQL can express the same intent with AccessLevel.USER_MODE. If a read or write genuinely belongs to platform plumbing, use system mode deliberately and make that elevation obvious in the class, method and tests.

3. Old security clauses must migrate

API 67.0 classes cannot retain WITH SECURITY_ENFORCED. Replace straightforward user-facing reads with WITH USER_MODE, then test filters, ordering, relationships and subqueries as well as selected fields.

User mode covers more of the operation. A query may now fail because a user cannot access a field in its WHERE clause. That is not a reason to hide the exception; it exposes a permission assumption the feature must resolve.

4. Triggers remain special

Triggers still run in system mode and cannot declare with sharing or without sharing. Their handlers therefore need intentional boundaries.

Some trigger behaviour is system-owned and should remain elevated. Other behaviour depends on what the initiating user can access. Separate those operations instead of assuming one mode fits the entire handler.

public with sharing class InvoiceTriggerHandler {
    public static Map<Id, Account> visibleAccounts(Set<Id> accountIds) {
        return new Map<Id, Account>([
            SELECT Id, Status__c
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ]);
    }
}

Do not mechanically force every trigger query into user mode. State which work belongs to the user and which belongs to the platform.

Upgrade deliberately

Before moving classes to API 67.0, search for:

rg "WITH SECURITY_ENFORCED" force-app/main/default
rg "Database\.query|Search\.query" force-app/main/default/classes
rg "insert |update |upsert |delete " force-app/main/default/classes

Then ask:

  • Does every class state its sharing model?
  • Does each important read and write state user or system intent?
  • Will fewer visible rows change the feature's meaning?
  • Do tests run as realistic non-admin users?
  • Is every elevated operation narrow and justified?

Secure defaults reduce accidental exposure, but they do not remove the need to understand the boundary. The best outcome of Summer '26 is not merely safer omitted keywords. It is a codebase where access decisions are visible and testable.

Sources