rei.r.joshua • July 1, 2026

One of the most important Apex changes in API v67.0 is also one of the easiest to underestimate:

Apex database operations now run in user mode by default.

That sentence sounds simple. It is not.

For years, Apex developers have lived with a security model where much of the code we wrote ran with elevated access unless we explicitly told it not to. That behavior made automation easier, but it also created a lot of implicit trust. Queries could return fields users could not see in the UI. DML could update fields users could not edit directly. Backend logic could quietly succeed because Apex was operating above the user’s normal access model.

API v67.0 changes that default. SOQL, SOSL, DML, and Database methods now default to user mode instead of system mode for code compiled at API version 67.0 and later. That means the running user’s object permissions, field-level security, and sharing rules are enforced unless the operation is explicitly run in system mode. Salesforce also recommends explicitly setting access mode on database operations so the intent is clear across API versions.

This is not just a syntax change. It is a design change.

The old model was:

code
// Usually system-ish unless we deliberately enforced security
List<Account> accounts = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
];

The new model is closer to:

code
// User context unless we deliberately elevate
List<Account> accounts = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
    WITH USER_MODE
];

And when the operation is intentionally part of trusted backend automation:

code
List<Account> accounts = [
    SELECT Id, Name, AnnualRevenue
    FROM Account
    WITH SYSTEM_MODE
];

That explicitness is the point.

The real change: security becomes the default, elevation becomes a decision

The most useful way to think about v67 is not “Salesforce changed how Apex works.”

It is: Salesforce changed the default trust boundary.

Before v67, Apex often assumed the developer had already made the right security decision. If you wanted object-level and field-level permissions enforced, you had to add that behavior with patterns like WITH SECURITY_ENFORCED, WITH USER_MODE, Security.stripInaccessible(), or describe checks.

In v67, Salesforce is moving the default closer to what most users assume already happens: if a user cannot see or edit something, code running on their behalf should not casually see or edit it either.

That is a better default. It is also exactly the kind of better default that can break old code.

Legacy Apex did not always bypass security because someone made a deliberate architecture decision. Often it bypassed security because that was simply how Apex worked. The dangerous part is not system mode itself. System mode is necessary. The dangerous part is accidental system mode.

API v67 reduces the amount of accidental elevation in the platform.

What changed in practical terms

There are three related changes worth treating as one architectural shift.

First, database operations now default to user mode in API v67.0 and later. That affects SOQL, SOSL, DML, and Database methods. In API v66.0 and earlier, those operations defaulted to system mode.

Second, Apex classes compiled at API v67.0 and later default to with sharing when no sharing declaration is provided. Previously, omitting the sharing keyword could leave code running without sharing depending on the context. In v67, an omitted sharing declaration is no longer a harmless omission; it changes behavior.

Third, WITH SECURITY_ENFORCED is removed in API v67.0 and later. Salesforce now directs developers to use WITH USER_MODE for user-context queries.

These changes all push in the same direction: make Apex secure by default and make elevated access explicit.

What did not change

System mode still exists.

That matters because the wrong reaction to this change is to treat every v67 failure as a permission bug. Sometimes the failure is telling you the code was relying on access it should not have had. Other times it is telling you the code is legitimate backend automation and should be explicit about running with elevated authority.

For example, a user-facing LWC controller that reads account data should generally respect the running user’s access.

A scheduled reconciliation job probably should not fail because the user who scheduled it lacks edit access to an internal processing field.

An invocable action that advances an order through a trusted business process may need to update fields that are not user-editable from the UI.

Those are different use cases. They deserve different access modes.

That is the architecture work v67 forces us to do.

Example 1: user-facing Apex should usually stay in user mode

Consider a simple Apex controller used by an LWC.

code
public with sharing class AccountSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            ORDER BY Name
            LIMIT 50
        ];
    }
}

In older Apex versions, this kind of query could return AnnualRevenue even if the user did not have field-level access to that field, unless the developer explicitly enforced security.

In API v67.0, the query defaults to user mode. If the user does not have access to AnnualRevenue, the query can fail instead of silently exposing the field.

A clearer version is:

code
public with sharing class AccountSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WITH USER_MODE
            ORDER BY Name
            LIMIT 50
        ];
    }
}

Yes, WITH USER_MODE may look redundant in v67.

I would still use it.

The goal is not to satisfy the compiler. The goal is to make the security contract readable. A future developer should not have to know the API version of the class to understand whether this query is meant to respect the running user.

Implementation takeaway: For user-facing reads, prefer explicit WITH USER_MODE. It documents intent, aligns Apex behavior with UI expectations, and avoids depending on implicit API-version behavior.

Example 2: system automation should be explicit about system mode

Now take a backend process. Imagine an invocable Apex action that marks order summaries as ready to invoice after a set of business validations.

The user may be allowed to trigger the process, but that does not necessarily mean they should have direct edit access to the internal status field being updated. The business process owns that transition.

A simplified version might look like this:

code
public inherited sharing class ReadyToInvoiceService {
    public static void markReadyToInvoice(Set<Id> orderSummaryIds) {
        List<OrderSummary> summaries = [
            SELECT Id, Status
            FROM OrderSummary
            WHERE Id IN :orderSummaryIds
        ];

        for (OrderSummary summary : summaries) {
            summary.Status = 'Ready To Invoice';
        }

        update summaries;
    }
}

Under API v67.0, this can fail if the running user does not have access to update OrderSummary.Status.

That failure is not random. It is the platform asking a design question:

Is this update being performed by the user, or by the system on behalf of a trusted business process?

If the answer is “this is trusted backend automation,” then the code should say so.

code
public inherited sharing class ReadyToInvoiceService {
    public static void markReadyToInvoice(Set<Id> orderSummaryIds) {
        List<OrderSummary> summaries = [
            SELECT Id, Status
            FROM OrderSummary
            WHERE Id IN :orderSummaryIds
            WITH SYSTEM_MODE
        ];

        for (OrderSummary summary : summaries) {
            summary.Status = 'Ready To Invoice';
        }

        update as system summaries;
    }
}

That one line matters:

code
update as system summaries;

It says the update is not meant to be constrained by the running user’s field-level access. The process is intentionally elevated.

That does not mean we ignore security. It means we move the security decision to the right layer. The user should still be authorized to initiate the process. The records should still be validated. The transition should still be constrained by business rules. But the internal status update itself does not need to pretend it is a direct user edit.

Implementation takeaway: Do not blanket-convert failing v67 code to system mode. Decide whether the operation is user-owned or process-owned. Then make that decision explicit in the code.

Example 3: Database methods need the same clarity

The same idea applies when using Database methods.

Before:

code
Database.SaveResult[] results = Database.update(summaries, false);

After, if this is intended to respect the running user:

code
Database.SaveResult[] results = Database.update(
    summaries,
    false,
    AccessLevel.USER_MODE
);

If this is trusted automation:

code
Database.SaveResult[] results = Database.update(
    summaries,
    false,
    AccessLevel.SYSTEM_MODE
);

The AccessLevel class defines user and system modes for Apex database operations. User mode enforces the current user’s object permissions, field-level security, and sharing rules. System mode ignores object and field-level permissions, while record sharing behavior is controlled by the class sharing context.

That last part is important. with sharing, without sharing, WITH USER_MODE, WITH SYSTEM_MODE, and AccessLevel are related, but they are not all the same lever.

Sharing is not the same thing as FLS

A common Apex security mistake is treating with sharing as if it solves the whole problem.

It does not.

with sharing controls record-level access. It determines whether the code respects sharing rules for which records the user can access.

It does not, by itself, enforce object permissions or field-level security. Salesforce’s docs call this out directly: with sharing and without sharing affect record-level security, not object-level or field-level security.

That distinction matters more in v67 because there are now two defaults changing in the same release:

code
public class MyService {
    // In API v67, omitted sharing defaults to with sharing.
}

And:

code
List<Account> accounts = [
    SELECT Id, Name
    FROM Account
    // In API v67, database operation defaults to user mode.
];

Those are separate concerns.

One controls record visibility.

The other controls how database operations enforce the user’s broader data access.

A clean v67 codebase should make both decisions visible.

code
public with sharing class AccountSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WITH USER_MODE
        ];
    }
}

Or, for intentional backend elevation:

code
public without sharing class InvoiceProcessingJob {
    public void execute() {
        List<OrderSummary> summaries = [
            SELECT Id, Status
            FROM OrderSummary
            WHERE Status = 'Approved'
            WITH SYSTEM_MODE
        ];

        for (OrderSummary summary : summaries) {
            summary.Status = 'Ready To Invoice';
        }

        update as system summaries;
    }
}

Neither version leaves the reader guessing.

What about stripInaccessible()?

WITH USER_MODE is strict. If the user does not have access to a queried field, the operation can fail.

That is usually correct for transactional logic. If the user is trying to perform an operation that requires a field they cannot access, failing loudly is better than quietly making up behavior.

But user interfaces sometimes need graceful degradation.

Maybe the component can show the account name and status, but hide revenue if the user cannot see it. In that case, Security.stripInaccessible() can be a better fit because it strips fields the user cannot access instead of forcing the whole operation to fail. Salesforce documents this as a way to enforce field- and object-level data protection while allowing more graceful handling of inaccessible fields.

Example:

code
public with sharing class AccountSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        List<Account> accounts = [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WITH SYSTEM_MODE
            ORDER BY Name
            LIMIT 50
        ];

        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.READABLE,
            accounts
        );

        return (List<Account>) decision.getRecords();
    }
}

This pattern should be used carefully.

The query intentionally runs with enough access to retrieve the fields, then strips anything the user cannot read before returning data. That can be valid when the service layer is trusted and the response is sanitized before it leaves Apex. It is not a license to query everything and hope for the best.

The practical rule is simple: use WITH USER_MODE when inaccessible data should fail the operation. Use stripInaccessible() when the experience should degrade safely.

Triggers are the exception, not the loophole

Apex triggers always run in system mode. Salesforce clarified that triggers bypass the current user’s sharing rules, field-level security, and object permissions, and they cannot declare sharing or access modes.

That does not mean trigger architecture gets a free pass.

Most serious trigger implementations delegate logic into handler classes. Those handler classes can still have sharing declarations, and the database operations inside them can still use explicit access modes.

This is where v67 should influence architecture patterns.

A trigger body should stay thin. It receives the platform event of record mutation. The handler should decide what kind of work is being performed.

For example:

code
trigger OrderSummaryTrigger on OrderSummary (after update) {
    OrderSummaryTriggerHandler.afterUpdate(Trigger.new, Trigger.oldMap);
}

Then:

code
public without sharing class OrderSummaryTriggerHandler {
    public static void afterUpdate(
        List<OrderSummary> newRecords,
        Map<Id, OrderSummary> oldRecordsById
    ) {
        InvoiceReadinessService.evaluate(newRecords, oldRecordsById);
    }
}

And inside the service:

code
public without sharing class InvoiceReadinessService {
    public static void evaluate(
        List<OrderSummary> newRecords,
        Map<Id, OrderSummary> oldRecordsById
    ) {
        List<OrderSummary> readyToUpdate = new List<OrderSummary>();

        for (OrderSummary summary : newRecords) {
            OrderSummary oldSummary = oldRecordsById.get(summary.Id);

            if (oldSummary.Status != 'Approved' && summary.Status == 'Approved') {
                readyToUpdate.add(new OrderSummary(
                    Id = summary.Id,
                    Status = 'Ready To Invoice'
                ));
            }
        }

        if (!readyToUpdate.isEmpty()) {
            update as system readyToUpdate;
        }
    }
}

The trigger itself runs in system mode, but the handler still tells us what it is doing. That is the difference between an intentional security model and an inherited accident.

The migration problem: old code was not written with this vocabulary

The hard part of v67 is not learning the syntax.

The hard part is reviewing code that was written before this distinction was obvious.

A lot of legacy Apex has no explicit sharing declaration. A lot of SOQL has no access mode. A lot of DML assumes that if the code compiles and the test passes, the security model is fine.

That is exactly the code v67 will expose.

A practical migration review should ask:

Does this class represent a user-facing interaction, backend automation, an integration boundary, or a mixed orchestration layer?

Should this query respect what the user can see, or does the process need broader read access?

Should this DML behave like a direct user edit, or is it an internal system transition?

If system mode is required, where is the authorization check that allows the user or process to initiate it?

Are we using without sharing because we need it, or because nobody ever wrote anything else?

Are tests running under realistic permission scenarios, or are they only proving that admins can do admin things?

That last one is the classic Salesforce test trap. A test that only exercises code as a highly privileged user is not proving the security model. It is proving that doors open when you use the master key. Useful, but incomplete.

A simple decision model

For v67 Apex, I would use this baseline:

User-facing read operations should use with sharing and WITH USER_MODE.

User-facing create, update, and delete operations should use user-mode DML unless there is a specific reason not to.

Backend automation should explicitly use system mode where the business process owns the operation.

Integration services should be reviewed based on the integration user’s permission model and the trust boundary of the external system.

Trigger handlers should not rely on trigger system mode as an excuse for vague service-layer access.

Security-sensitive UI responses should prefer failing closed with WITH USER_MODE, unless graceful degradation is an intentional part of the experience.

None of that is especially exotic. It is mostly what good Apex architecture already looked like. v67 just makes the platform less forgiving when the code is ambiguous.

The anti-pattern: “just add system mode”

There will be a very tempting migration strategy:

The code broke after moving to API v67. Add WITH SYSTEM_MODE and update as system until it works again.

That is not a migration strategy. That is a rollback wearing a fake mustache.

System mode should be used when the process genuinely requires elevated access. It should not be used to preserve old behavior without understanding why that behavior existed.

The better migration is slower but safer:

First, identify what failed.

Second, determine whether the failure is correct.

Third, choose the access mode based on ownership of the operation.

Fourth, make the class sharing declaration explicit.

Fifth, add or update tests that prove both the allowed and restricted paths.

That is how this change becomes an architectural improvement instead of another release-note scramble.

Final thought

API v67 does not remove system mode from Apex.

It removes the comfort of not thinking about it.

That is a good thing.

Salesforce orgs accumulate automation over years. Controllers become services. Services get called from flows. Flows get called from agents. Batch jobs reuse methods originally written for buttons on page layouts. Before long, nobody is entirely sure whether a piece of Apex is acting as the user, the system, an integration, or some blurry combination of all three.

v67 forces that question into the code.

That is the real value of the change. Not that every operation should run in user mode. Not that every backend process should be locked down until it stops working. The value is that access mode becomes part of the design, not a side effect of the runtime.

In a mature Salesforce codebase, that is exactly where it belongs.