Designing Permissions: Why Hardcoded Roles Break at Customer Five
Moving from a single role to a permission model that survives growth, without every customer request becoming a code change.
Every platform starts with two roles: owner and user. Then a customer asks for "an assistant who can see revenue but not edit courses", and another wants "a grader limited to one cohort". If roles are hardcoded, each request becomes a release.
Separate the role from the permission
The common mistake is checking the role directly: `if (user.role === 'admin')`. That couples every point in your codebase to a closed list. Check the permission instead: `can(user, 'course.publish')`. Roles become bundles of permissions, and a customer can compose their own without you touching a line.
A permission is not a boolean
- The action: what are they doing? (view, create, edit, delete, publish)
- The resource: to what? (course, learner, invoice)
- The scope: which instances? (all courses, their own, one cohort's)
Most systems handle the first two and forget the third, then end up writing exceptions into every query. Make scope part of the permission model from the start, even if you launch with a single scope.
Deny must be the default
Anything not explicitly granted is forbidden. That sounds obvious until you ship a feature and forget to attach a permission to it — if the default is allow, you just opened it to everyone silently. Make a missing definition an error you see in development, not a silence you discover in production.
A new feature with no permission attached should fail, not succeed for everybody.
Check at the right layer
Hiding a button is not access control, it is user experience. The real check happens server-side at every endpoint, and at query level whenever scope depends on the data. Mirror it in the interface for usability, and never rely on that copy.
Make it auditable
As teams grow, "who changed this?" becomes a daily question. Log every change to permissions themselves and every sensitive action, with actor, timestamp, and before/after values. You will rarely need it; when you do, nothing substitutes for it.
The original model is described by NIST's role-based access control project, and Google's Zanzibar paper is the reference for fine-grained, resource-level permissions at scale. For the mistakes that actually recur, OWASP's authorization cheat sheet is a short checklist.
If you are designing a permission model for your own product, the moment to review it is before your first enterprise customer, not after. Talk to us.
FAQ
When should I move from RBAC to ABAC?
When permission starts depending on data attributes rather than identity alone — "can see requests belonging to their branch", for instance.
Should customers create custom roles?
Yes if you sell to organizations. Start from editable presets rather than an empty screen.
Where do permissions live?
In the database with a short cache. Defining them in code makes every customer request a deployment.