July 14, 2026 · Architecture · Backend
Designing role-based access for multi-stakeholder platforms
When several different kinds of user share one system, the tempting shortcut is a permissions flag on everything. It doesn't survive the third role. Here's the structure that does.
03 From building ILMSA platform with two user types can often get away with an `is_admin` boolean and a handful of `if` statements. A platform with five or six genuinely different stakeholder groups — each with a different subset of the same workflows — cannot, and building it that way is how you end up with permission checks scattered across dozens of route handlers, each slightly inconsistent with the others.
ILMS needed exactly that: several distinct kinds of institutional user, each seeing only the part of the workflow that belonged to their role, without any of them tripping over UI or API surface meant for someone else.
The structure that holds up
- Model roles and permissions as data, not code — a role is a named set of permissions, checked once at the boundary, not re-derived per feature
- Check permissions at the API boundary, not scattered through business logic — a handler either has the required permission for the action it's about to take, or it doesn't get that far
- Keep the interface role-aware at the routing level, not just visually — a hidden button that's still reachable by URL is not access control, it's a suggestion
- Treat "what can this role see" and "what can this role do" as separate questions — read access and write access don't have to follow the same shape
Where it's tempting to cut corners
The shortcut that looks fine early and becomes a liability later is checking roles by name in scattered conditionals — `if user.role == "supervisor"` sprinkled through the codebase. It works until a new role needs supervisor-like permissions but isn't literally a supervisor, and now you're either duplicating the conditional everywhere it appears or refactoring under pressure. A permission model that's data from the start absorbs a new role as a new row, not a code change.
The extra structure costs more up front than the `if` statement does. It's worth it exactly when you already know there's a third role coming — which, on an institutional platform, there almost always is.