> For the complete documentation index, see [llms.txt](https://docs.appfarm.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.appfarm.io/reference/security/permissions/designing-secure-permissions.md).

# 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**](/reference/security/permissions.md) **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**](/reference/security/permissions.md#object-classes) **(including** [**Conditional Permissions**](/reference/security/permissions/conditional-permissions.md)**) 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.&#x20;

**Environment access is separate from data access.** A role must be granted login access per environment under [Permissions -> Login](/reference/security/permissions.md#login-access). 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](/reference/security/permissions.md) 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.&#x20;

{% hint style="info" %}
Conditional permissions may affect performance in large data sets. If you need advanced conditional permissions or conditional permissions on large data sets, you may contact Appfarm if performance becomes an issue.
{% endhint %}

### 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**](/reference/security/permissions/conditional-permissions.md) (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](/library/action-nodes/run-service.md) under an elevated identity, without giving the calling role that identity's data permissions. The role needs the "[Act as service account](/reference/security/permissions.md#accounts-and-roles)" 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](/reference/security/service-accounts.md#api-keys) belongs to a [service account](/reference/security/service-accounts.md) 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

<table data-search="false"><thead><tr><th>Pitfall</th><th>Why it goes wrong</th></tr></thead><tbody><tr><td>Granting Read "temporarily" to unblock development</td><td>It is rarely revoked, and it exposes the whole object class</td></tr><tr><td>Relying on a data source filter to hide other users' records</td><td>The filter is app configuration, not access control</td></tr><tr><td>Reusing one broad custom role across several apps</td><td>The user gets the union of everything that role can ever do</td></tr><tr><td>Giving an unauthenticated role Read on business data</td><td>The unauthenticated session is effectively public</td></tr><tr><td>Broad "Act as service account" grants</td><td>Elevation should be to one specific account, for one specific purpose</td></tr><tr><td>Forgetting Aggregate</td><td>Aggregate can leak counts and sums from data the role cannot otherwise read</td></tr><tr><td>Leaving GraphQL, the API explorer or user account manipulation enabled in Production</td><td>Each widens the surface a session can reach</td></tr><tr><td>Assuming Owners/Maintainers/Developers covers client testing</td><td>Those roles do not grant client access outside Development</td></tr><tr><td>Putting secrets in Files or in the client</td><td>Files are publicly accessible by URL; secrets are server-side only and unavailable in the Appfarm Client</td></tr></tbody></table>

### Designing for Unauthenticated Access

Please read the [Configure unauthenticated access](/how-to/authentication-and-access-control/unauthenticated-access.md) article on how to configure unauthenticated access, and most importantly, the [Best Practices](/how-to/authentication-and-access-control/unauthenticated-access.md#best-practices) section in the same article.

### A practical checklist

1. List the roles a real user can hold, and write down the union of their permissions. That is their actual access.
2. For every object class with Read granted to a non-internal role, ask: is it acceptable for that role to retrieve every record?
3. Where the answer is no, apply conditional permissions (authenticated) or a service with Run as service account (either).
4. Keep internal, elevated roles off users entirely. They exist only for service accounts.
5. Re-run the review whenever an object class, app, service, flow or role is added.
6. Test by acting as the role, not as yourself, and probe beyond the intended flow.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.appfarm.io/reference/security/permissions/designing-secure-permissions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
