Designing Secure Permissions
Appfarm enforces access control on the server. Understanding what that means in practice, and what it does not cover, is the difference between a solution that is secure by design and one that only appears secure in the UI.
How permissions are evaluated
Permissions are granted to roles, never to users directly. A user or service account is assigned one or more roles, and their effective access is the sum of all permissions across those roles. There is no "deny" that overrides a grant. If any role a user holds grants Read on an object class, that user can read it.
Access follows the user, not the app. When someone logs in, their permissions travel with them for as long as they stay logged in. Those permissions are not tied to the app they happen to have open. This matters because your app is just one way of reaching the data. The same login can also reach it through the API explorer, through GraphQL, or through a request sent directly to the platform by someone with the right technical skills. If the user's roles allow reading an object class, they can read all of it, whether or not your app ever shows it.
Think of permissions as the lock on the door and the app as the hallway that leads to it. Designing a hallway that doesn't pass the door doesn't lock it.
Object class permissions (including Conditional Permissions) are the real boundary. Create, Read, Update, Delete and Aggregate are evaluated server-side per object class and cannot be tampered with from the client. Everything else you build in the app layer is presentation.
Environment access is separate from data access. A role must be granted login access per environment under Permissions -> Login. Built-in roles (Owners, Maintainers, Developers) give access to Appfarm Create, but do not give client access to Test, Staging or Production. A custom role is always required for end users of those environments.
New things start with no access, and you have to grant it. When you create an object class, only the built-in roles (Owners, Maintainers, Developers) can touch it. When you create a custom role, it starts with no data access at all. That is the safe default, but it means access never appears on its own. Every time you add an object class, an app, a flow or a service, someone has to go through the permissions and decide who should reach it. Make that a step in your routine before anything moves toward Production.
What is not a security boundary
These are useful for performance and user experience, but none of them protect data:
Data source filters. Server-side filters on database-connected data sources do limit what is transmitted, which matters for performance and reduces exposure, but they are part of the app definition. A session with Read permission can query the same object class through other paths.
Client filters, UI visibility conditions, and disabled or hidden components. All evaluated in the browser.
Not exposing an app. Hiding an app from the app list does not restrict the underlying data.
Obscure identifiers alone. A random identifier is a reasonable secret when it is the only way to reach a single record through a narrow interface. It is not protection if the caller also has general Read on the object class.
If a requirement is "user A must never see user B's data", it has to be expressed in object class permissions, conditional permissions, or a service, never in the UI.
Read is the permission to be careful with
Create and Update are naturally scoped: the caller supplies the object and can only affect what they supply (assuming your data model requires the right references). Read is different. Granting Read on an object class exposes every object in it to that role, and there is no partial Read at the object level unless you use conditional permissions.
This asymmetry drives most secure designs:
Grant Create and Update freely where the write is self-contained.
Grant Read narrowly, and prefer to route it through something that can constrain it.
Broad Read is sometimes an acceptable tradeoff
Not every object class needs the full treatment. Narrowing Read costs design time and adds moving parts, and applying it everywhere makes solutions harder to build and maintain. For an internal role, granting Read on an entire object class is often a reasonable decision.
The question to ask is not "would these users misuse it?" but "if every record in this object class ended up in the wrong hands, how bad would that be?" Intent is the wrong thing to plan around. The realistic scenarios are an account that gets compromised, someone who leaves the company on bad terms, or a well-meaning user who discovers the API explorer and looks around. None of those require anyone to set out to break your app.
Broad Read is usually fine when the data is internal and low-consequence: reference data, product catalogues, internal project lists, anything most of the organisation could see anyway.
Narrow it when:
The object class holds personal data. Data protection obligations apply regardless of how trusted the audience is, and "everyone internal can see everything" is difficult to defend as data minimisation.
The data is sensitive between colleagues: salaries, performance reviews, health information, HR cases, disciplinary records.
The data is commercially sensitive: pricing, margins, negotiations, unreleased plans, customer contracts.
The role is held by externals, contractors or partners, even if they are named individuals you trust.
A full extract would be a reportable incident. If your answer to "what if all of it leaked?" involves lawyers or a supervisory authority, the tradeoff has already been decided for you.
Where the tradeoff is acceptable, write down why. A short note on the role or in your own documentation ("Read on Project is intentional; internal reference data, low sensitivity") turns an implicit assumption into a reviewable decision, and saves the next person from either undoing it or copying it somewhere it doesn't belong.
Where it isn't acceptable, the next section covers how to narrow it.
Three patterns for narrowing access
Conditional permissions (premium) attach a filter to an object class permission per role, enforced server-side. This is the primary mechanism for data silos in multi-tenant solutions, for example Person.Company = Current User.Company. It requires a logged-in user, so it does not apply to unauthenticated access.
Run as service account lets a client action run a service under an elevated identity, without giving the calling role that identity's data permissions. The role needs the "Act as service account" permission for that specific service account, and access to the service. This is how you turn a broad Read into a single-record lookup: the service holds the read permission, and the endpoint decides what it will return.
Service endpoints and API keys apply the same idea to third parties. The API key belongs to a service account whose role is scoped to exactly what that integration needs, and the endpoint controls the shape of the input and output.
In all three cases the logic that constrains access runs on the server, where it cannot be modified by the caller.
Common pitfalls
Granting Read "temporarily" to unblock development
It is rarely revoked, and it exposes the whole object class
Relying on a data source filter to hide other users' records
The filter is app configuration, not access control
Reusing one broad custom role across several apps
The user gets the union of everything that role can ever do
Giving an unauthenticated role Read on business data
The unauthenticated session is effectively public
Broad "Act as service account" grants
Elevation should be to one specific account, for one specific purpose
Forgetting Aggregate
Aggregate can leak counts and sums from data the role cannot otherwise read
Leaving GraphQL, the API explorer or user account manipulation enabled in Production
Each widens the surface a session can reach
Assuming Owners/Maintainers/Developers covers client testing
Those roles do not grant client access outside Development
Putting secrets in Files or in the client
Files are publicly accessible by URL; secrets are server-side only and unavailable in the Appfarm Client
Designing for Unauthenticated Access
Please read the Configure unauthenticated access article on how to configure unauthenticated access, and most importantly, the Best Practices section in the same article.
A practical checklist
List the roles a real user can hold, and write down the union of their permissions. That is their actual access.
For every object class with Read granted to a non-internal role, ask: is it acceptable for that role to retrieve every record?
Where the answer is no, apply conditional permissions (authenticated) or a service with Run as service account (either).
Keep internal, elevated roles off users entirely. They exist only for service accounts.
Re-run the review whenever an object class, app, service, flow or role is added.
Test by acting as the role, not as yourself, and probe beyond the intended flow.
Last updated
Was this helpful?