Back to BlogArchitecture & Engineering

Building a Roles and Permissions UI for Your SaaS

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

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.

ConceptExampleWho defines itChanges how often
Permissioninvoice.createYou, in code, as features shipRarely; one per capability
System roleOwner, Admin, MemberYou, as sensible defaultsRarely
Custom role"Billing clerk" = invoices onlyThe customer's admin, in the UIOften, per customer
AssignmentMaria is a Billing clerkThe customer's adminConstantly

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?

OptionExamplesChoose this whenWatch out for
Authorization-as-a-serviceHosted fine-grained authorization servicesYou need policy-based access at scale and want a managed decision engineAnother service on the request path; latency and outage behaviour to check
LibraryAn open-source RBAC or policy library in your stackYou want the check logic solved and your own data and UIYou still build the admin UI and the matrix yourself
Build it inThe model and matrix aboveRoles are core to your product and pricing, and customers need custom rolesYou own the escalation and last-owner guards; test them hard
Hire a team to build itRAITHub or another studioPermissions, the admin UI and the audit trail must agree and be provenInsist 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.

RBACRoles and permissionsPermissions UIAuthorizationSaaSPostgreSQL

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.