---
title: "Enforcing CRUD and FLS for LWC"
url: "https://trailassembly.com/apex-crud-fls-in-lwc/"
topic: "Apex and Development"
published: "2026-09-19T14:22:04+00:00"
updated: "2026-10-06T13:59:05+00:00"
error_codes: "AuraHandledException"
---

# Enforcing CRUD and FLS for LWC

**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.

```apex
@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:

```apex
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.

```apex
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.

```apex
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 user` or 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 `AuraHandledException` with 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.

---

Source: https://trailassembly.com/apex-crud-fls-in-lwc/
Trail Assembly — independent Salesforce write-ups.
