User roles sound simple when you describe them as a list.
A user can do one set of things. A manager can do more. An administrator can do everything.
Then you start designing the actual application and realize that people do not always fit neatly inside those labels.
A manager may need to manage employees, review company information and upload documents. But that same manager may also need to complete their own training as a regular user.
An administrator may need complete control over the platform, but they can still need access to the same functionality available to everyone else.
That creates a more interesting problem than simply asking, “What role is this user?”
This is one of the challenges that comes with custom web application development. Once different people can interact with the same system in different ways, the application needs to understand more than whether someone is simply logged in or logged out.
A role describes someone's level of access. It does not always describe everything that person needs to do.
The role system started looking more like a tree
When I started designing the different experiences, I realized the permissions made more sense as a hierarchy than as completely separate types of accounts.
At the base is the regular user. They can access their personal dashboard, complete training, take quizzes, access certificates, use the functionality available to them and manage their own experience.
A manager needs those same abilities, but also needs another layer of functionality for managing their company.
They can manage employees and subcontractors, work with company forms, purchase or manage applicable training and see a higher-level overview of what is happening within their company.
An administrator sits above that with much broader access across the platform, including administrative functionality that would never make sense for a company manager, such as newsletter management and system-wide controls.
There is also a much more restricted subcontractor experience. That role exists for a very specific reason: allowing a subcontractor to access company forms without giving them the rest of the company's functionality.
The result is less like four unrelated boxes and more like a tree where permissions expand or narrow depending on what someone actually needs.
A manager is still a user
This became one of the most important decisions in the architecture.
It would have been possible to treat managers as an entirely separate kind of user.
They could have had their own course logic, their own components and potentially even a separate login or account experience.
But that would create duplication almost immediately.
A manager who needs to complete their own training does not need a special “manager version” of a course.
They need the course.
So managers use the same learner architecture as regular users. Their personal course assignments work through the same system, and when they want to complete their own training, they can go to their personal dashboard.
Their management capabilities exist in addition to that experience, not instead of it.
Shared behaviour should stay shared just because the person using it has additional permissions.
Avoiding the separate-account problem
Separating every responsibility into a different account might make the role definitions look cleaner technically, but it can create a much messier experience for the person using the application.
Imagine being responsible for managing company training and also needing to complete a course yourself.
You should not need to log out of your manager account, sign into a learner account and maintain two separate identities just because the application decided those actions belonged to different categories.
That would also mean maintaining duplicate logic and potentially duplicate components for functionality that is fundamentally the same.
Instead, one account can expose different parts of the application according to what that person is allowed to do.
The user does not need to think about which role they are using
There are multiple ways for someone with additional permissions to move between their personal experience and the areas they manage, including navigation throughout the application.
But I do not think the person should have to consciously think, “Now I am acting as a manager” or “now I am acting as a learner.”
If they want to complete their own training, the personal dashboard is there.
If they want to manage their company, the company functionality is there.
The interface should make the appropriate pathway obvious.
Permissions should reflect what someone is allowed to do. The interface should reflect what they are actually trying to do.
Not every permission needs to become visible UI
One of my preferences when designing these experiences is to avoid filling the interface with functionality someone cannot use.
If a role does not need access to something, I generally hide it rather than displaying an unavailable control.
A subcontractor does not need to see a collection of disabled management features.
They need the company forms they are allowed to access.
Similarly, a manager does not need administrative controls simply so the application can show them everything that exists.
Removing irrelevant choices can make a complicated application feel considerably smaller.
The complexity of the application does not need to determine the complexity of every person's interface.
Hiding a button is not a permission system
There is also an important technical distinction between what the interface displays and what someone is actually permitted to do.
Permissions are handled on both sides of the application. This is also one of the areas I consider during a website security review: what someone can see in the interface and what the system actually allows them to access are two different things.
The frontend controls the experience. It determines which navigation items, actions and interfaces are appropriate for that user.
But removing an administrative button from the screen does not secure the functionality behind that button.
The backend also needs to validate whether the person making a request is allowed to perform that action.
Routes and actions that a user is not permitted to access are blocked rather than relying entirely on the interface to keep them away.
The frontend can hide the door. The backend still needs to lock it.
Role is only part of the permission question
Knowing that someone is a manager still does not tell the application everything it needs to know.
It also needs to know what they manage.
A company can have multiple managers, but being a manager does not mean someone should have access to every company in the system.
Company membership provides another boundary around the data they are permitted to access.
The company ID is used to keep company-specific information associated with the appropriate organization.
This becomes especially important when the application contains employee information, training, forms, certificates and other company-specific records.
So the permission question is not simply:
Is this person a manager?
It is also:
Are they a manager of this company?
The middle role became the hardest one
Interestingly, the most difficult role has not necessarily been the one with the most permissions.
It has been the manager.
Administrators have broad access. Regular users have a much more focused experience.
Managers live in the middle.
They need some functionality that administrators have, but not all of it. They also need the functionality of a normal user.
That makes it easier for an edge case to appear where something was originally considered an administrative feature but a manager also legitimately needs access to it.
It has made role checks an important part of adding new functionality.
When something new is introduced, the question is not only whether the feature works.
I also need to consider:
- Which roles should see it?
- Which roles should be able to use it?
- Which data should each role be able to access?
- Does company membership create an additional restriction?
- Is this shared functionality or does this role genuinely need something different?
New roles are a good test of the original architecture
The subcontractor role was introduced later in the application's development in response to a client need.
Its requirements were much narrower than the existing roles.
A subcontractor needed access to company forms, but did not need the broader management functionality available to managers.
This is one of the reasons I prefer thinking about what someone actually needs to do rather than treating a role name as the architecture itself.
Applications change. New users appear. Existing responsibilities change. A role system that only makes sense for the exact users the application has today can become difficult to extend later.
That same principle came up while extending the course architecture itself. In Designing a Better Learning Experience for a Multi-Section Online Course, I look at how a new course structure was added without rebuilding the experience for the rest of the training library.
Shared components still need context
Sharing functionality does not mean every role has to receive an identical interface in every situation.
This is also where a well-planned design system and component library becomes useful. Shared patterns can remain consistent across the application without requiring every user or every situation to receive an identical interface.
Sometimes it makes sense to reuse the same component and expose different actions depending on permissions.
Other times the context is different enough that a separate component is cleaner.
I do not think there needs to be a rigid rule that every shared feature must use one component or that every role deserves its own.
The better question is whether sharing the implementation actually reduces unnecessary duplication without making the component difficult to understand.
The biggest mistake is making roles more complicated than they need to be
Role systems can become complicated very quickly because there are so many possible combinations of people, permissions and actions.
That does not mean every possible distinction needs its own role, dashboard, login and collection of components.
My preference is to start with what people need to do, share what can reasonably be shared and restrict the functionality that actually needs to be restricted.
The underlying implementation can use roles, boolean permissions, company relationships and backend validation to make those decisions.
The person using the application does not need to know about any of that.
A good permission system should almost disappear
When this is working properly, I do not think someone should be particularly aware that a large permission system exists behind the interface.
When an application has grown over time, this is also something a UI/UX Audit can help uncover: whether different users are being shown the right information, actions and pathways for what they actually need to accomplish.
They should sign in and see the things they are allowed to do.
They should not see irrelevant functionality. They should not need a second account to perform another part of their job. They should not have to understand why another type of user sees something different.
They should simply be able to do their work.
The system can understand roles. The user should only have to understand what they can do next.