Skip to content
All writing
Architecture

Four layers of permissions, and why three of them are not enough

Multi-tenant B2B software fails on access control before it fails on features. The permission model SiteLeader uses, and the reasoning behind each layer.

Most B2B platforms start with a role column: admin, manager, staff. It survives until the first customer says their site supervisor should see labour attendance but not vendor rates, and only for the two sites they run. At that point a role enum cannot express what the business means.

Layer one — the coarse role

Still useful, just not sufficient. It answers what kind of user this is: platform operator, tenant owner, staff, external client. It decides which application they see at all.

Layer two — module grants per tenant

Not every customer buys every module. A contractor on a basic plan should not see equipment management at all — not a disabled button, not an empty page. Grants live on the tenant, so the platform operator controls what a customer's whole organisation can reach.

Layer three — granular custom roles

This is where most systems stop, and it is the layer customers actually want to configure. The tenant owner builds their own roles from individual permissions, because only they know that at their company the storekeeper approves material issue but not purchase.

Layer four — row scope and field visibility

The layer people forget, and the one that causes data leaks. Two supervisors may have identical permissions and still must not see each other's sites. That is not a permission question, it is a data-scope question — which rows, not which screens.

Field visibility is the same idea one level down. A supervisor may need the labour attendance record but not the wage rate on it. Same row, different columns.

// Tenant isolation belongs in middleware, never in each handler.
// One forgotten filter in one endpoint leaks another customer's data.
app.use(builderScope);

function builderScope(req, res, next) {
  req.scope = { builderId: req.user.builderId };
  next();
}

Tenant isolation must be enforced in one place. If every query author has to remember it, one of them eventually will not — and that is a breach, not a bug.

The order matters

Each layer narrows the one above it and can never widen it. A custom role cannot grant a module the tenant has not bought; row scope cannot expose another tenant's data. Enforce that direction strictly and the model stays reasonable to audit as it grows.

The system behind this article

SiteLeader — Multi-tenant SaaS platform

Multi-tenant SaaS for Indian building contractors — labour, materials, finance, GST billing and AI-assisted design in one tenant-isolated platform.

Read the case study