Engineering Note Product Engineering

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.

Start reading 4 min read · July 23, 2026

The idea in one picture

Roles, scopes and delegation meet at request time.

  • Person decides
  • Automated
In
  • Access request
  • Organization graph
  • Live delegations
Out
  • Allow or deny
  • Actions tagged to both
01

Assign role over scope

An admin grants a role over a subtree of the organization graph. Partner admins can only grant from a restricted catalog and cannot see brand-side roles like Approver.

Person decides

A person makes this call. The system gives them the context.

02

Delegate for a period

A holder grants part of their own role over part of their own scope to someone else. It cannot exceed what they hold, and it is gone when it expires.

Person decides

A person makes this call. The system gives them the context.

03

Rebuild closure table

Every ancestor and descendant pair is stored with the dates it holds, rebuilt transactionally when partners are transferred or reorganized.

Automated

Rule-based and automatic, logged in the audit trail.

04

Check the permission

Does the user hold a role with this permission whose scope contains the target, directly or through a live delegation? Support staff get no exception.

Automated

Rule-based and automatic, logged in the audit trail.

05

Nightly consistency check

The denormalized table can be wrong. The nightly check has twice caught a bug in a transfer routine before it affected anyone.

Automated

Rule-based and automatic, logged in the audit trail.

06

Periodic access review

The brand reviews partners and brand-side scopes. Each partner reviews its own users. An access model nobody reviews degrades quietly.

Person decides

A person makes this call. The system gives them the context.

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.

Where this applies

Put it to work.

Newsletter

Get the next piece by email.

One email a month with new research, playbooks and engineering notes.

Newsletter

New pieces, once a month.

Engineering notes, playbooks and research. No promotions, unsubscribe in one click.

Next step

Working on something like this?

We are happy to compare notes. Tell us where you are stuck.

Your first seven days are on us. Plan, strategy and solution architecture, before any commitment.

Not ready to talk? Take the 8 minute readiness assessment