Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
A roles and permissions UI lets your customer's admin decide who can do what inside their account. Build it on permissions as the atomic unit, roles as named bundles of permissions, a matrix the admin can actually read, and one server-side check the UI only mirrors. The screen is the easy part; the trap is shipping a UI that shows access the backend never enforces.
If you would rather have roles and permissions built for you, see how RAITHub would build this below.
What is a roles and permissions UI, and why is the backend the real work?
The UI is the screen where an admin assigns roles to people and, in richer products, edits what each role can do. Behind it, the authorization must already be real: every action gated by a permission check on the server. If the backend hard-codes role === 'admin' in forty routes, no UI can safely let an admin invent a fourth role. So the data model and the check come first, and the screen renders what they already allow.
This post is the UI layer on top of role design. The model itself, roles versus permissions versus tenant scope, is in SaaS authorization and RBAC design; read that first if you have not settled it.
What is the right unit: roles or permissions?
Permissions are the unit; roles are a convenience on top. A permission is a single verb on a resource ("invoice.create", "member.remove"). A role is a named set of permissions an admin can assign in one click. Checking permissions, never role names, is what lets you add a role or change a plan without editing code in dozens of places.
| Concept | Example | Who defines it | Changes how often |
|---|---|---|---|
| Permission | invoice.create | You, in code, as features ship | Rarely; one per capability |
| System role | Owner, Admin, Member | You, as sensible defaults | Rarely |
| Custom role | "Billing clerk" = invoices only | The customer's admin, in the UI | Often, per customer |
| Assignment | Maria is a Billing clerk | The customer's admin | Constantly |
RAITHub built this at scale on Sundor Skin, a B2B wholesale platform with 12 staff roles composed from 88 discrete permission codes across 146 PostgreSQL tables, with row-level security and 530+ tests. That scale is only manageable because the checks are against permission codes, not role names.
What should the data model and the check look like?
Store permissions a role grants, and the roles a user holds. The effective permission set is the union of the user's roles' permissions. The check asks one question: does this user hold this permission in this workspace?
CREATE TABLE role_permissions (
workspace_id uuid NOT NULL, -- NULL for system roles shared by all
role_key text NOT NULL,
permission text NOT NULL,
PRIMARY KEY (workspace_id, role_key, permission)
);
CREATE TABLE user_roles (
workspace_id uuid NOT NULL,
user_id uuid NOT NULL,
role_key text NOT NULL,
PRIMARY KEY (workspace_id, user_id, role_key)
);
// One check, called on the server for every gated action. The UI mirrors it.
export async function can(db: Tx, userId: string, workspaceId: string, permission: string) {
const row = await db.one(
'SELECT 1 FROM user_roles ur ' +
'JOIN role_permissions rp ON rp.role_key = ur.role_key ' +
'AND (rp.workspace_id = ur.workspace_id OR rp.workspace_id IS NULL) ' +
'WHERE ur.user_id = $1 AND ur.workspace_id = $2 AND rp.permission = $3 LIMIT 1',
[userId, workspaceId, permission],
)
return row != null
}
Cache the user's permission set per request, and invalidate it when their roles change. The browser may hide a button, but only this server check decides.
How do you make the permissions matrix usable, not overwhelming?
An admin faced with 88 raw permission toggles will give up and grant everything. Group permissions and show roles as columns.
- Group by feature area. Show "Billing", "Members", "Reports" as sections, each with its permissions, not one flat list.
- Roles as columns, permissions as rows. A matrix of checkboxes reads at a glance: which roles can do what. Lock the system roles' rows so an admin cannot accidentally strip the owner's powers.
- Start from a template. A new custom role copies an existing role, then the admin removes what it should not have. Starting from blank is where mistakes live.
- Show the effect. When an admin changes a role, show how many people it affects before they save.
Every change an admin makes here is a security-relevant event. Record it, with who changed what, in the audit trail, as described in SaaS audit log design, and surface the readable version in an activity feed users trust.
Which permission mistakes cause real incidents?
These are the ones that leak data or lock people out. Guard each.
- UI-only enforcement. A hidden button is not a permission. Anyone can call the API directly, so the server check is the only real gate.
- Privilege escalation. A user who can edit roles must not be able to grant themselves a permission their own role lacks. Require a strictly-higher role to edit roles.
- The last owner. Block removing the final permission that lets anyone manage the account.
- Stale sessions. Re-read permissions per request, not from a token minted at login, or a revoked user keeps access until it expires.
Buy, build or hire?
| Option | Examples | Choose this when | Watch out for |
|---|---|---|---|
| Authorization-as-a-service | Hosted fine-grained authorization services | You need policy-based access at scale and want a managed decision engine | Another service on the request path; latency and outage behaviour to check |
| Library | An open-source RBAC or policy library in your stack | You want the check logic solved and your own data and UI | You still build the admin UI and the matrix yourself |
| Build it in | The model and matrix above | Roles are core to your product and pricing, and customers need custom roles | You own the escalation and last-owner guards; test them hard |
| Hire a team to build it | RAITHub or another studio | Permissions, the admin UI and the audit trail must agree and be proven | Insist the handover includes the privilege-escalation tests, not only the screen |
Do-it-yourself estimate: 5–10 days for the permission model, the single check, the grouped matrix UI, custom roles and the escalation guards, if auth and memberships exist. The main risk is a UI that implies access the backend does not enforce.
How RAITHub would build this
As part of a new SaaS build, or added to an existing product, scoped in writing after the free audit.
- Permission catalogue: one code per capability, grouped by feature area, with system-role defaults.
- Custom roles: admins create roles from a template, edit a readable matrix, and see who each change affects.
- One server check: every gated route and job goes through it; the UI only mirrors what it allows.
- Guards and audit: privilege-escalation and last-owner guards, with every role change written to the audit log.
Timeline: inside a new product, this is part of the 4–6 week fixed-scope SaaS build; added to an existing backend, it fits the 6–12 week backend range, with the exact scope in the quote.
You receive: automated tests and CI for the authorization checks and escalation guards, handover docs, and full IP assigned to you under NDA.
Next step: a free 15-minute technical audit, then a written fixed quote. See the SaaS development service, or book the audit.
Frequently asked questions
Should my code check roles or permissions?
Permissions. Checking role names scatters pricing and policy logic across the codebase, so every new role means code changes in many places. Checking a permission code means a role change is a data change, not a deploy.
Do I need custom roles, or are fixed roles enough?
Fixed system roles (owner, admin, member) cover most early products. Custom roles matter once customers ask to limit someone to one area, such as billing only. Build the permission model first; custom roles are then a UI on top of it.
How do I stop an admin granting themselves more access?
Require a strictly-higher role to edit roles, and never let a user grant a permission their own role lacks. Without that guard, anyone who can edit roles can quietly make themselves an owner.
Is hiding a button enough to enforce a permission?
No. The UI is a convenience; anyone can call your API directly. The permission check must run on the server for every gated action, and the UI should render from the same check so the two never disagree.
How many permissions is too many?
There is no hard cap; Sundor Skin runs 88. The limit is usability, not the model. Group permissions by feature area and let admins start custom roles from a template, so a large catalogue stays manageable in the UI.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.