Permissions are part of the product


The first version of permissions in a lot of products is a check in the interface. If the user is not an admin, do not show the button. That feels done, because the person you are demoing to cannot see the action. It is not done. Anyone who can talk to the API can still perform it. The screen and the server are telling two different stories, and the server is the one that counts.

I built this properly on a multi-tenant product with several kinds of people in it: a super admin, an admin, a standard user, and an advisor. There were more than twenty distinct permissions, spread across users, companies, plans, and the parts of the system only some roles should touch. A single “is admin” flag could not carry that. An advisor is not a lesser admin. They need a different slice of the product, and a tenant boundary on top of the role.

I started from the list of actions, not from the list of roles. An action is something a person does: invite a user, change a plan, see another company’s data, export a report. Roles are bundles of those actions. If you start from role names, you end up with checks like “if admin or super admin or sometimes advisor,” copied into every screen, and they drift. If you start from actions, a role is just a set you can edit, and the check is always “does this person have this permission?”

That check has to exist in two places, and they have to mean the same thing. In the interface I used it to protect routes and to decide what to render. A person without access should not be dropped into a page of controls that all fail. They should land somewhere that matches what they are allowed to do. On the server, the same permission guarded the endpoint. A filter on the controller is a good place for that, because a new action cannot “forget” the check as easily as a check buried inside one service. The interface is a courtesy. The API is the rule.

The admin screen for those permissions is part of the product too. If changing access means a deploy, nobody will keep the rules honest. I built a place to manage roles and see the permissions update, including the unglamorous cases: a bulk invite from a CSV, duplicate rows, a row that fails validation without taking the whole file down with it. Those are not extras. They are how a real company gets its people in.

The failure I watch for is a permission that exists in the database and not in the interface, or the other way around. Someone adds a feature, hides it for the wrong role, and leaves the endpoint open. Or the endpoint is locked and the button is still there, so the product looks broken instead of intentional. I treat a new action as unfinished until both sides name it.

Twenty permissions sounds like a lot until you list the things the product actually allows. It is cheaper to name them early than to discover, after launch, that “admin” meant three different jobs and you can no longer tell them apart.