Salesforce CRUD and FLS: The Demo That Lied to You

CRUD, FLS, and the Demo That Lied to You

If you built the app, you are probably a system administrator in the org where you built it — which is exactly why missing Salesforce CRUD and FLS enforcement never shows up in your demo.

The model knows that. It writes queries that work for you. It does not write `WITH USER_MODE`. It does not check field-level security. It does not think about the partner user, the community user, or the intern in sales.

The demo is true for one profile. That is not the same thing as a security model.

Why this is 25 points

CRUD, FLS, sharing, and secrets are 25 points of Finish Score. It’s the largest single dimension, the one the Finish page calls Security posture, because it is how unfinished apps become incident reports. Salesforce’s own Top 20 Vulnerabilities Found in the AppExchange Security Review ranks object and field-level security as the number one reason apps fail, by a significant margin. Hard-coded secrets are a separate item on the same list.

Both failure modes have the same shape in AI-generated Apex: the code works in a developer org. The builder is an admin. The query returns rows. The LWC renders. Nothing in the happy path tells you that a customer-community user can see a field they should not, or that an API key is sitting in a Custom Setting that is not a Named Credential.

Code Analyzer v5 will catch a lot of this if you run it. Most apps that are 60% built have not run it with the AppExchange rule selector. Many have not run it at all.

The patterns we keep seeing

These are representative patterns, not named-client findings.

– Classes generated without sharing, or with no sharing keyword at all, so every user sees every record. Sharing controls which records a user sees, not which objects and fields. Apex runs in system mode either way, so with sharing alone does not enforce CRUD or FLS.
– No WITH USER_MODE (or the older WITH SECURITY_ENFORCED) on queries, and no AccessLevel.USER_MODE on DML. These are guards the builder never needed, because the builder is an admin.
– `@AuraEnabled` methods callable by a profile that should only see a subset of fields
– Guest-user or Experience Cloud paths that inherit the builder’s assumptions
– Passwords, tokens, or endpoint URLs in Custom Settings, Custom Metadata, or — worse — in the class
– Debug statements that print records you would not want in a log
– Two automations on the same object, only one of which honors sharing

Any two of those and you do not need a pep talk. You need a score.

This is a representative pattern, reduced to the smallest version that shows the failure:

What the AI wrote
// Works for the admin, leaks for everyone else
public without sharing class ContactController {
    @AuraEnabled
    public static List<Contact> getContacts() {
        return [SELECT Id, Name, Email, Salary__c
                FROM Contact];
    }
    @AuraEnabled
    public static void saveContacts(List<Contact> contacts) {
        update contacts;
    }
}
What we ship
// Sharing, object, and field security enforced
public with sharing class ContactController {
    @AuraEnabled
    public static List<Contact> getContacts() {
        return [SELECT Id, Name, Email, Salary__c
                FROM Contact WITH USER_MODE];
    }
    @AuraEnabled
    public static void saveContacts(List<Contact> contacts) {
        Database.update(contacts, AccessLevel.USER_MODE);
    }
}

A representative pattern, reduced to the smallest version that shows the failure. Highlighted: the three changes that enforce security.

Signs the app is not finished

A short list. Be honest with yourself:

– It only works when you run it
– The only tests are happy-path.
– There is a password or a key that is not a Named Credential
– Two triggers on the same object, written three weeks apart, by two different chats
– You would not let a new admin inherit this repo
– You have not run Code Analyzer v5 with the AppExchange rule selector
– “We will document it after go-live”

Prove it in five minutes

You don’t need a consultant to find out whether your demo is lying. You need a user who isn’t you.

Create a test user on the Minimum Access profile and give them only the permission set a real end user would get. Log in as that user, or have an admin use Login As, and click through the app. Anything that shows a field they shouldn’t see, or saves a record they shouldn’t touch, is a CRUD/FLS gap.

To make it permanent, put it in a test:

Test: a restricted user cannot read Salary__c
@IsTest
static void restrictedUserCannotReadSalary() {
    Profile p = [SELECT Id FROM Profile
                 WHERE Name = 'Minimum Access - Salesforce'];
    User u = new User(
        LastName = 'Restricted', Alias = 'rstrct',
        Email = 'restricted@example.com',
        Username = 'restricted' + DateTime.now().getTime() + '@example.com',
        ProfileId = p.Id, TimeZoneSidKey = 'America/New_York',
        LocaleSidKey = 'en_US', EmailEncodingKey = 'UTF-8',
        LanguageLocaleKey = 'en_US'
    );
    insert u;
    System.runAs(u) {
        try {
            ContactController.getContacts();
            Assert.fail('A restricted user should not be able to read Salary__c');
        } catch (QueryException e) {
            // Expected: WITH USER_MODE blocked the query
        }
    }
}

If that test passes against the AI-written version, you don’t have a security model. You have a demo.

AppExchange is a different sport

An internal app that works for your company and a managed package that works for other companies are not the same deliverable with a bigger invoice.

Finish for AppExchange exists because the extra work is real:

– Namespace and packaging strategy
– Second-generation managed packages
– Install and upgrade in a clean scratch org
– License Management App wired to actual enforcement in code
– A Security Review packet that does not waste a 4–9 week queue

If you framed a product you want on the listing, do not “just package it on a Friday.”

Start with the Audit. If the verdict is rebuild, that is cheaper news on day 6 than on day 60.

Review-and-submit only starts at $9,500 if the app is already close. A full AppExchange build is a different number because the work is a different shape. Confirm any Salesforce review *fee* with your Partner Account Manager in writing before you treat a number as settled — Salesforce has published more than one figure on more than one page.

What “enforcing who sees which field” looks like in a Sprint

For internal apps, Finish Sprint is the engagement that closes the CRUD / FLS gaps, removes hard-coded secrets, consolidates the trigger-and-Flow tangle, and puts tests around the behavior you actually care about. Typical band is $25K–$50K over 4–8 weeks, with a floor for small scope and a higher band when the org is genuinely complex.

We are not rewriting your business idea. We are rewriting the seams so the intern in sales does not inherit a sysadmin query.

FAQ

**Why does my demo work if FLS is missing?**
Because you are almost certainly running it as an administrator, and admins can see every object and field. The AI writes queries that work for whoever is testing, which is you. Log in as a restricted user and the same app will either show data it shouldn’t or throw errors it never handled.

**What’s the difference between with sharing and user mode?**

with sharing controls which records a user can see, based on the org’s sharing rules. User mode (WITH USER_MODE on queries, AccessLevel.USER_MODE on DML) controls which objects and fields they can access. You need both. A class can be with sharing and still expose a salary field to every user who can see the record.

**How do I enforce CRUD and FLS in Apex?**

Salesforce’s current recommendation is user mode: add WITH USER_MODE to SOQL and pass AccessLevel.USER_MODE to Database methods. For records you need to clean before saving or returning, Security.stripInaccessible() removes the fields the user can’t access. Older code often uses WITH SECURITY_ENFORCED or manual isAccessible() checks. Those still work, but user mode is the modern standard.

**Is Code Analyzer enough?**
It is the right free scanner. It is not a substitute for a verdict, a score, and a scoped fix.

**Do internal apps need the AppExchange rule selector?**
Not to list. Running it anyway is cheap and usually educational.

**What is Finish for AppExchange versus Finish Sprint?**
Sprint ships an internal app. AppExchange adds packaging, LMA, subscriber-org behavior, and review readiness.

 


Email john@cloudfirstlabs.com with “Finish Score” in the subject, or book a 15-minute Finish Audit call from the Finish page.
Built with AI. Finished by engineers.

About CloudFirst Finish

CloudFirst Finish takes a 20–90% built Salesforce app — Apex, LWC, Flow — and makes it documented, tested, and deployable. Finish Audit is $4,500 and five to seven days: a 0–100 Finish Score and a keep/refactor/rebuild verdict. Sprint, AppExchange packaging, and Watch sit behind that score. Prices and the ladder live on cloudfirstlabs.com/finish.

About CloudFirst Labs

CloudFirst Labs is a Tampa Salesforce consulting and AppExchange partner. The same bench that finishes AI-started apps is the bench that connects the Salesforce CRM, PSA, and billing systems MSPs run every day — ConnectWise, Autotask, ServiceNow, NetSuite, Certinia — so closed deals flow into delivery and invoices match what was sold. Co-founded by a former VP of Technology at a $200M MSP. We have lived the integration problems we still solve. Finish is the last 40% of an app. The MSP practice is the revenue engine those apps sit inside. Different jobs. Same production standard.

Need the MSP lane instead? Book an MSP Revenue Assessment.

Connect with John Dionne on LinkedIn and follow CloudFirst Labs for Finish and MSP systems writing, or browse more on the CloudFirst Labs blog.