We built permissions the way everyone builds permissions. Three roles: admin, member, guest. A matrix of what each could do. It took a quarter and it was wrong within a month.
Nobody asked for a role
The support tickets were never about roles. They were about a specific person and a specific thing. Can our contractor see this one project but not the billing page. Can the design team edit these files but not delete them. A role is our answer to that question, not theirs.
Resources, then roles
We moved the permission boundary from the account to the resource. Access is granted on a workspace or a project, and roles became a convenient default rather than the model itself. The matrix shrank because most of it had been combinations nobody used.
The migration
We ran both models in parallel for a release, wrote every existing role assignment as an equivalent set of resource grants, and compared the two on every request for two weeks before we cut over. There were nine mismatches. All nine were bugs in the old model.
If you are about to build roles, spend a week reading support tickets first. The shape of the model is in there.
