App user roles and permissions only count where the data lives.
Most app user roles and permissions begin as one admin flag on the user record, read by the app to decide which buttons to draw. That holds until somebody watches the network traffic. A screen is a suggestion. The only check that counts is the one running where the data is kept, which is the part of app security basics people skip.
This guide covers naming actions before naming roles, where the check has to live, how the role reaches that check, and the cases that break a role model three months after launch.
See what a hidden button still allowsThe short version
A role is a bundle of permissions, and a permission is a check on the server.
Write the list of actions first. Who can delete a record somebody else created, who can export the whole customer list, who can change another person's role. Roles are names for groups of those answers, which is why four roles chosen after the list beats twelve chosen before it.
Then put every one of those checks behind the network call, in your own server code and in the database, and let the app hide controls purely as a courtesy. A permission enforced only in the app decides what is easy to do. It does not decide what is possible.
Name the actions before you name the roles
A permission is a verb, a resource and a scope. Not delete, but delete an invoice. Not edit an invoice, but edit an invoice I created, as against edit any invoice in the workspace. The scope is the part people leave out, and it is where homemade models fall over, because a manager and a member usually differ by scope on the same verb rather than by having separate screens.
So draw the grid. Actions down the side, the people who do them across the top, one yes or no in each cell. Most teams finish with four names: an owner who can also close the account, an admin who manages other people, a member who does the daily work, and a viewer who reads. Anything past that is normally one action that needs its own flag, not a fifth role. Watch the word admin while you do it. Admin roles in an app tend to bundle two separate jobs, managing other people and seeing everything, and plenty of teams want the first without the second. Role based access control in an app this size is four rows and a scope column, not a framework.
Where the roles themselves live is the next decision, and it is cheaper to settle it before the first migration. Roles written into the app source mean every customer gets the same four names and every change to them is a new release for everybody. Roles in a table mean you can add a dispatcher next quarter without shipping anything, and mean a customer can be given a name that matches their own org chart.
The shape that survives is a membership row rather than a column on the user: the person, the thing they belong to, and their role in it. That row is also the thing your database rules will join against, so it is worth getting right early. The rest of the table decisions this forces sit in mobile app database design.
A hidden button is not a permission
Be plain about this, because it is the whole point of the page. The app is code you handed to the user. Anyone can read the bundle, watch the request the export screen makes, and send that request themselves with their own valid login token. Hiding a control changes what is convenient. It changes nothing about what the server will do when it is asked.
The check therefore has to live behind the network call, and there are two useful places for it. In your own server code, where the handler refuses the request before it touches a row. And in the database, where the rule sits next to the data and catches anything that arrives by another path, including a script, a future admin tool and whatever you build next year. In Postgres the second one is row level security: you switch it on per table, then write policies deciding which rows a role may read or change.
Backend vendors say this in their own documentation rather than leaving it to be inferred. Supabase, whose managed Postgres is one common way to get row level security, warns that a table in an exposed schema without it enabled is readable and writable by any role holding a grant on that table, and that adding policies does not remove those grants. Because the rule is a Postgres feature rather than library code, it also holds when the data is reached through other tooling. Firebase makes the mirror image of the argument for its own rules: they are defined outside your app, so clients are not the thing responsible for enforcing security.
One honest cost. Enforcing in two places means writing the rule twice, and two copies drift. Pick the database as the authority on who may see and change which rows, keep your server code for workflow rules such as an approval that has to happen in order, and treat the app's copy as cosmetic. If the three ever disagree, the database is right.
Supabase docs, row level security
Try it
What a hidden button still allows
Assume the app hides all four of these from a viewer. Switch on the ones you also check behind the network call.
4 of 4 still possible
Anyone holding a valid login token can still do 4 of these by sending the request directly, whatever the app draws for them.
How the role reaches the check
A check has to know the caller's role, and there are only two ways it finds out. Either the role is a claim inside the signed login token, or it is a row the backend reads on every request. Both are used in production. They fail differently, and the difference matters more than the performance.
A claim is fast, because the token is already in the request and its signature proves nobody edited it. It is also stale. A token issued before you demoted somebody carries the old role until it expires or is refreshed. Firebase is specific about the mechanics here: custom claims are set only from a privileged server environment through the Admin SDK, the payload must not exceed 1000 bytes, and the new claims reach the client's token when the user signs in again or the app forces a refresh. Its documentation also says claims are for access control and not for storing other data. Treat a claim as a cache with an expiry, never as the record.
A role read from a table on each request is always current and costs one read. That is usually the right trade for user permissions in a mobile app, where a demotion needs to bite now rather than at the next token refresh. Whichever you pick, the writing side has one rule: the client never sets its own role. If your sign up flow lets the app write a profile field, the role does not belong in that field.
Roles also decide what a device is allowed to keep. A cached table on a phone holds the rows the user could see at the last sync, and a local copy cannot re-check a permission that changed this morning. If your app works offline, read mobile app offline sync next to this one, because demoting somebody has to clear what their device already has as well as what the server will send.
What breaks a role model in month three
The same person in two places. A consultant who is an admin in one workspace and a viewer in another cannot carry a role on their user record, because the role belongs to the pair: this person, in this workspace. Putting the role on the user and discovering this later is the most common rewrite in this area, and it reaches every query you have written by then.
Temporary access. Somebody covers a colleague for a week, an auditor needs read access until Friday. If the answer is to change a role and remember to change it back, it does not get changed back. An expiry column on the membership row, read by the same rule that reads the role, costs one column now and saves an awkward conversation later.
The last owner. Any screen that removes a person or lowers a role needs a rule refusing to leave the account with nobody able to administer it. Put it with the other checks rather than in the screen, because the screen is not the only thing that will call it. The same goes for the invite flow: an invitation is a pending membership, so it needs a role on it and an expiry, or it becomes a quiet way in months later.
Finally, the record of changes. Who granted this, who removed it, when. A role change is the edit people argue about afterwards, so store it as an event with an actor and a timestamp instead of overwriting a field. It costs one table, it answers the question an auditor actually asks, and it is the only way to tell a mistake from a misunderstanding.
What each place to put the check protects
| Where the check runs | Stops a request sent by hand | Reflects a role change at once | Works per record | Changing a rule needs |
|---|---|---|---|---|
| Hiding the control in the app | No | No | No | an app store release |
| A check in your own API code | Yes | if it reads the role table | Yes | a backend deploy |
| Row level security in the database | Yes | if the policy reads the table | Yes | a policy change |
| A role claim inside the login token | Yes | No | only where a rule maps it | a token refresh |
| Roles written into the app source | No | No | No | a new build for everyone |
Building roles into an app you own
If you are choosing a stack rather than inheriting one, roles decide more than they look like they do. You need somewhere to keep the membership table, somewhere to write the rule that reads it, and a way to change that rule without pushing a new binary to every phone. A tool that gives you screens and no server leaves you with the first problem unsolved and the second impossible.
Newly is an AI app builder: you describe the app in plain English and it writes a real React Native and Expo project you own, runs it on a cloud iPhone or Android simulator while it builds, and ships it to TestFlight and to Google Play internal testing. Plans start at $25 a month and there is no free plan. Its backend, added when an app needs accounts or data shared between people, includes an API service for your own server code and endpoints plus a Postgres database for each environment, which is where the checks above belong. Worth stating plainly: its own documentation does not describe a built in roles or permissions model, so ask for the membership table and the server side rules in your own words and do not assume they appear by themselves.
Roles stop being abstract the moment they map to a job someone does on a Monday. For a picture of roles doing real work, look at how a request, an approval and a manager view divide up in practice, then write your grid from that rather than from the four names everybody starts with.
Questions people ask about app roles and permissions
A permission is one action on one kind of thing, with a scope: edit any invoice, or edit only the invoices I created. A role is a named bundle of permissions, such as admin or viewer. Write the permissions first. The roles are just names for the groups you end up with, which is why the list of actions is the real design work.
Write the permission grid, then build the app around it
List the actions, decide who is allowed each one, and keep every check where the data is. Describe that in plain English and build the app from it.
Start building