Here is a sentence that surprises almost every beginner I teach: by default, Apex ignores your security model.
You spent all that time setting up profiles, permission sets, and field-level security. You carefully hid the Salary__c field from the Sales profile. And then someone writes ten lines of Apex, a sales rep clicks a button, and the code reads that field anyway. No error. No warning. The data just flows.
That is not a bug. It is how Apex has always worked, and understanding it — then fixing it with two small words — is one of the highest-value habits you can build early. Let me walk you through it.
System mode: what Apex does by default
Apex runs in what Salesforce calls system mode. In system mode, your code executes with the permissions of the system, not the person who triggered it. Concretely, that means:
- Object permissions are ignored. Your code can query and update objects the running user cannot even see a tab for.
- Field-level security is ignored. Hidden fields are readable and writable.
Why would the platform do this? Because sometimes it is genuinely what you want. A trigger that stamps an audit field needs to write that field even for users who cannot edit it by hand. An integration class may need to touch objects no human profile touches. System mode is a feature — but it is a feature that is on by default, and that is where beginners get burned.
The mental model I give my students: when Apex runs, the platform hands your code a master key to the whole building. Every door opens. It is your job, as the author of that code, to decide whether the code should really be using the master key or the key ring of the person who pressed the button. Most of the time — almost any time a real user is on the other end — you want the user’s key ring.
(One important nuance: sharing — which records you can see — is a separate dial. We will come back to it, because you usually need both dials set.)
The old way: checking permissions by hand
For years, respecting the user’s permissions meant doing the checks yourself. You would litter your code with describe calls:
// The old way: manual FLS checks (verbose and easy to forget)
if (Schema.sObjectType.Contact.fields.Salary__c.isAccessible()) {
List<Contact> contacts = [
SELECT Id, Salary__c FROM Contact WHERE AccountId = :accountId
];
// ... and hope you checked every field you queried
}
Later, Salesforce gave us Security.stripInaccessible, which at least did the field-by-field work for you:
// Better, but still an extra step you have to remember
List<Contact> contacts = [
SELECT Id, Salary__c FROM Contact WHERE AccountId = :accountId
];
SObjectAccessDecision decision =
Security.stripInaccessible(AccessType.READABLE, contacts);
List<Contact> safeContacts = decision.getRecords();
Both approaches work, and you will still meet them in older codebases. But notice the shape of the problem: security was something you added afterward, one check at a time. Forget one field, one object, one code path, and the hole is silently open. Security that depends on developers remembering things is fragile security.
The modern way: WITH USER_MODE and “as user”
Today the fix is two words. In SOQL, you append WITH USER_MODE:
// The modern way: the query itself enforces object and field security
List<Contact> contacts = [
SELECT Id, Salary__c
FROM Contact
WHERE AccountId = :accountId
WITH USER_MODE
];
Now the query runs as the user. If the running user cannot read Salary__c, the query throws a clear QueryException instead of quietly leaking the data. If they cannot access Contact at all, same thing. You do not check permissions — the platform does, at the moment of access, for every field in the statement.
DML gets the same treatment with as user:
// Insert respecting the user's create permission and FLS on every field
Contact c = new Contact(LastName = 'Aksu', Salary__c = 90000);
insert as user c;
// Works on the Database methods too
Database.update(contactsToSave, AccessLevel.USER_MODE);
If the user is not allowed to write Salary__c, the DML fails loudly. Loud failure is a gift: you find the problem in testing, not in an audit.
Compare the before and after honestly. The old way was fifteen lines of ceremony per operation. The new way is two words at the end of the statement. When the safe thing is also the easy thing, people actually do it — that is why I consider USER_MODE the biggest quiet win in Apex security in years.
”But I already wrote with sharing” — the two dials
This is where I have to slow down, because it trips up even experienced developers. You may remember from my security fundamentals post that Salesforce security answers two separate questions: what you can do, and what you can see. Apex has one dial for each, and they do not overlap.
with sharingon a class controls record access — which rows come back, based on org-wide defaults, the role hierarchy, and sharing rules. It says nothing about fields or object permissions.WITH USER_MODE(andas user) controls object and field access — whether the user may touch this object and these fields at all. Since it also respects sharing, it covers the row question too, but only for the statements you mark.
So a class declared with sharing can still happily read a field the user is not allowed to see. And a plain query with WITH USER_MODE inside a class you forgot to declare leaves your other queries in that class wide open. The pattern I teach, and the pattern I use in every class I ship, is both together:
public with sharing class ContactService {
public static List<Contact> myContacts(Id accountId) {
return [
SELECT Id, Name, Email
FROM Contact
WHERE AccountId = :accountId
WITH USER_MODE
];
}
}
with sharing sets the default posture for the class; WITH USER_MODE enforces field and object security on each statement. Two dials, both set.
Why this matters more than ever in 2026
Here is the part that turned this from “good practice” into “non-negotiable” for me.
In 2026, it is no longer just users clicking buttons that run your Apex. AI agents do. An Agentforce action, or an MCP tool calling into your org, executes your Apex on behalf of a real user — but the user never sees the code path, never reviews the query, and often did not phrase the request precisely. The agent decided which method to call and with what arguments.
If that Apex runs in default system mode, you have handed the master key to a language model. Whatever the AI asks for, the code can fetch — including the fields and objects the actual human was never allowed to touch. That is not a hypothetical; it is exactly the kind of quiet over-permission that security reviews now hunt for.
WITH USER_MODE is the fix, and it is a beautiful one: no matter how creative the AI gets, the action physically cannot exceed the permissions of the human it is acting for. I use this pattern in every agent action I ship, without exception. The AI can be wrong; the permission boundary cannot.
Your next step
Build the habit now, while your muscle memory is still forming: every SOQL query gets WITH USER_MODE, every DML gets as user, every class gets with sharing — and you deliberately remove them only when you have a written reason to run in system mode (audit stamps, integration users, and similar cases).
If you are still getting comfortable with queries themselves, start with SOQL for beginners, and if you have not written your first class yet, begin here. The rest of the beginner path lives in Foundations.