Almost every product starts with three roles. Admin can do everything, member can do most things, viewer can look. It is a good default and it survives longer than people expect.
It breaks in a predictable way. A customer wants someone who can manage billing but not touch production. Someone who can invite people but not remove them. Someone who can read the audit log and nothing else. Each request is reasonable, and each one adds a role, and eleven roles later nobody can say what any of them mean.
Separate the noun from the label
The fix is to stop treating a role as a thing your code checks. Your code should check permissions — narrow, verb-shaped capabilities like invoices:read or members:remove. A role is a named bundle of those, and the name is for humans.
This sounds like extra indirection until the first customer asks for a role you did not anticipate. With permissions, that is a configuration change. With hardcoded roles, it is a release.
Your code checks permissions. A role is a name humans give a bundle of them.
Design the permission set deliberately
Permissions should map to things a person can do in your interface, not to database tables. If a permission does not correspond to something a customer can point at, it will be assigned wrongly.
Keep the set small enough to fit on a page. Thirty well-chosen permissions are manageable. Three hundred generated from your schema are a configuration screen nobody will ever get right, and the failure mode is an administrator granting too much because working out the minimum is too hard.
- Name permissions after user-visible capabilities, not tables
- Ship sensible default roles — most customers will never change them
- Let the ones who care define their own without asking you
- Put permissions in the session, so a check does not cost a round trip
Where roles live
In a multi-tenant product, a role is not a property of a person. It is a property of a membership: the same person can be an administrator in one organisation and a viewer in another, and switching between them must issue a session that reflects it.
Getting that boundary right early is much cheaper than retrofitting it, because the alternative — a global role on the user record — leaks across tenants in ways you will find out about from a customer.
Everything here, already built
Sign-in, enterprise SSO, directory provisioning, roles and an audit trail behind one API. Start with the quickstart and have a working sign-in this afternoon.