The short version: a Lightning web component enforces nothing. If it reads data through an Apex controller, that controller is the only thing standing between a user and data they should not see, and Apex does not check object or field permissions unless you tell it to. If it reads data through a Lightning Data Service wire adapter, the platform does the checking for you, which is why the wire adapter is the safer default.
Where the responsibility actually sits
An @AuraEnabled method is a public endpoint. Anyone who can load a page in the org can call it directly, with any parameters they like, regardless of whether your component would ever send those parameters. The component’s markup is not a control surface.
Three separate gates have to be closed in the controller: whether the user may touch the object at all, whether they may read or write the specific fields, and whether they may see the specific records. A class marked with sharing closes only the third.
The modern way: user mode
User mode enforces object permissions, field-level security and sharing together, in one keyword, regardless of the class sharing declaration. It is the right default for anything an LWC calls.
@AuraEnabled(cacheable=true)
public static List<Account> search(String term) {
String pattern = '%' + String.escapeSingleQuotes(term) + '%';
return [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Name LIKE :pattern
WITH USER_MODE
LIMIT 50];
}
For DML, the equivalents are the as user modifier or the access level overload:
update as user accounts;
Database.update(accounts, AccessLevel.USER_MODE);
Dynamic SOQL takes the same treatment through Database.query(soql, AccessLevel.USER_MODE). The older WITH SECURITY_ENFORCED still works and covers less. If you are touching the code anyway, move it.
When you need to filter rather than fail
User mode throws when the user cannot see a field. Sometimes that is right and sometimes you want to return what they can see and drop the rest. Security.stripInaccessible does that.
SObjectAccessDecision d = Security.stripInaccessible(
AccessType.READABLE, accounts);
return d.getRecords();
It is also the tool for sanitising records coming in from the component before DML, using AccessType.CREATABLE or UPDATABLE. Never trust an SObject that arrived from the client.
Why the wire adapter is safer
Lightning Data Service goes through the User Interface API, which applies object permissions, field-level security and sharing on the server before anything reaches the browser. There is no controller for you to get wrong.
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import NAME from '@salesforce/schema/Account.Name';
export default class AccountTile extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [NAME] })
account;
}
In practice that means getRecord, getRecords, createRecord and updateRecord from lightning/uiRecordApi, and the base components built on them: lightning-record-form, lightning-record-edit-form and lightning-record-view-form. A field the user cannot read simply does not render, and no code of yours decided that.
The cost is flexibility. LDS is record-centric and does not do arbitrary queries, aggregates or multi-object joins. The sensible rule: use LDS for anything that is one record or a small known set of records, and write Apex only when the requirement genuinely exceeds it. Most components reaching for Apex did not need to.
A review checklist for @AuraEnabled classes
- Class declares
with sharing. Inherited sharing is acceptable only where the caller is known and documented. - Every query uses
WITH USER_MODE, or there is a written reason it does not. - Every DML uses
as useror an explicit access level. - Records arriving from the client are stripped before being written.
- Ids passed from the client are validated as belonging to the expected object and the expected parent.
- Dynamic SOQL escapes user input, and bind variables are used wherever possible.
- Exceptions surface as
AuraHandledExceptionwith a message that does not leak field names the user cannot see.
The Ids point deserves emphasis. The most common real vulnerability in custom LWC work is not a missing permission check, it is a method that accepts a record Id from the client and does something with it in a class that runs without sharing.
Frequently asked
Does with sharing enforce field-level security? No. It governs record visibility only. Object and field permissions need user mode or explicit checks.
Is cacheable=true a security setting? No. It enables client-side caching and forbids DML in the method. It has no bearing on permissions.
Should I check isAccessible() manually? Rarely now. User mode covers the common cases more reliably than hand-written describe checks, which drift as fields are added.
What about guest users? Same rules, higher stakes, plus the guest user sharing model on top. Test every public component as an actual guest rather than reasoning about it.