Designing role-based access for a two thousand partner network
Why most RBAC models break at the second tier of hierarchy, and how we ended up treating roles, scopes and delegation as three separate things.
July 23, 2026 · 4 min read · Principal Engineer
Role-based access control is easy for a hundred users in one company. You define roles, assign people to them, and check the role on every request. The model breaks the moment the users are not your employees, and it breaks again at the second tier of hierarchy. We learned both lessons on a partner network of around two thousand organizations, and this is what we ended up with.
Where the simple model fails
The first version of most RBAC schemes has roles like Partner Admin, Partner User, Regional Manager, Approver. A user has a role; a role has permissions; done.
The first crack appears when a Regional Manager for the west should approve partners in the west and not the east. The role is the same; the scope is different. Teams usually solve this by creating Regional Manager West and Regional Manager East. With eight regions and six roles that is forty-eight roles. With sub-regions it is hundreds, and nobody can say what any of them mean.
The second crack is hierarchy on the partner side. A distributor has dealers beneath it. The distributor’s admin should see its dealers’ onboarding status but not their bank details, and should be able to nominate a dealer without approving it. The dealer’s admin should see its own users and nothing above it. Now a permission depends on the relationship between two organizations, and a flat role cannot express that.
The third crack is delegation. The compliance head goes on leave and wants her deputy to approve for two weeks, within her scope, with the audit trail showing that it was delegated. Copying her role onto the deputy grants too much for too long and records nothing.
Roles, scopes and delegation as separate things
We split the model into three parts that combine at request time.
A role is a bundle of permissions with no scope: approve-partner, view-bank-details, edit-documents. There are about a dozen of these and they have not changed much.
A scope is a subtree of the organization graph: a region, a distributor and its dealers, a single partner. Scopes are not stored on roles. They are stored on the assignment: this user holds this role over this scope. The same person can hold Approver over the west region and Viewer over the whole network.
A delegation is a time-boxed assignment created by a holder, granting a subset of their own role over a subset of their own scope to another user, with an expiry and a reason. The delegator cannot grant more than they have. When it expires it is gone, and every action taken under it is tagged with both people.
The permission check on a request becomes: does this user hold a role containing this permission, whose scope contains the target, either directly or through a live delegation? That question is answered by a single query against a materialized closure table of the organization graph, which we rebuild when the graph changes rather than on every request.
The organization graph
The partner network is a tree, mostly. Brand at the root, regions below, distributors below regions, dealers below distributors. Except when it is not: a distributor operating in two regions, a dealer transferred between distributors with history that must stay visible to the old one for a period.
We modeled it as a directed graph with typed edges and effective dates, and gave up on the tree assumption early. The closure table stores every ancestor-descendant pair with the date range over which it holds. Most of the difficulty in the whole system lives in maintaining this table correctly through transfers and reorganizations.
Partner-side administration
Two thousand organizations means we cannot manage their users. Each partner has an admin who creates users, assigns them partner-side roles and deactivates leavers. Partner-side roles are a restricted catalog: a partner admin can grant Partner User but cannot grant, or even see, Approver.
The consequence is that access reviews happen at two levels. The brand reviews which partners exist and which brand-side users hold which scopes. Each partner reviews its own users. We built the review as a periodic task with sign-off, because an access model nobody reviews degrades quietly.
Trade-offs
Scoped assignments make the admin interface harder. Granting someone access is now a three-part decision, and we spent real time on the screen that makes that legible.
The closure table is denormalized and can be wrong. We rebuild it transactionally on graph changes and run a nightly consistency check against the edge table. Twice the check has caught a bug in a transfer routine before it affected anyone.
We chose to deny by default with no exceptions, including for our own support staff. Support access is a role with a scope like any other, granted for a ticket and expiring with it. This caused friction in the first month and has since prevented the kind of standing superuser access that shows up in every audit finding.
What we would do differently
Design the delegation feature on day one. We added it in the second phase after watching people share passwords to cover leave, which is exactly the behavior a good access model is supposed to make unnecessary. The model needed it from the start; we simply had not asked who covers for whom.
And we would have written the permission check as a library used by every service from the beginning, rather than letting two services implement it slightly differently for six weeks. That inconsistency was found by a reviewer, not by a test, and it should have been found by a test.