[readonly] markdown buffer
Secure-by-default Apex in Summer '26
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.