WhollySoftware
Back to blogWeb

Building a Customer Portal With Role-Based Access

Wholly Software TeamJanuary 20, 20268 min read
Building a Customer Portal With Role-Based Access

A B2B insurance client needed a customer portal supporting five distinct roles — account owner, billing admin, claims manager, standard user, and a read-only auditor role required for compliance — across organizations that ranged from single-person accounts to enterprise clients with dozens of seats and nested sub-teams. A simple isAdmin boolean, which is what their previous prototype used, had no path to supporting this without a rewrite, so we built the permission model properly from the start.

We implemented role-based access control with permissions expressed as discrete capabilities (claims.view, claims.file, billing.edit, users.invite) rather than roles themselves being checked directly in code. Roles are just named bundles of capabilities, defined in a database table rather than hardcoded, which mattered in practice within two months when the client needed a new 'claims viewer' role that was a subset of claims manager permissions — that was a data change, not a code change.

Enforcement happens in three layers, which felt like overkill until the second layer caught a real bug. API routes check capabilities server-side on every request using middleware that resolves the user's role and required capability before the handler runs — this is the actual security boundary. The UI also checks capabilities to hide or disable actions a user can't perform, which is a UX layer only, not security, and we were explicit with the client that anything enforced only client-side is not actually protecting data. Row-level checks in the database queries themselves form the third layer, scoping every query to the user's organization ID so even a bug in the middleware couldn't leak cross-tenant data.

The nested sub-team structure for enterprise accounts was the trickiest part — an account owner needed visibility into all sub-teams, a claims manager needed visibility into their own sub-team only, and permissions needed to inherit sensibly down a hierarchy without requiring the client's admin to manually configure access for every sub-team individually. We modeled this as a materialized path on the organization tree, which let a single query resolve 'everything this user can see' rather than recursive lookups on every request.

Auditability was a compliance requirement, not a nice-to-have — every permission change, role assignment, and access to claims data gets logged to an append-only audit table with actor, target, timestamp, and the specific capability exercised, queryable by the auditor role itself so the client's compliance team could self-serve reports instead of filing a request with engineering.

The role model has held up through eighteen months of the client's growth, including the addition of two new roles and a reseller/partner account type we hadn't originally scoped, both handled as configuration rather than requiring a schema change — which is really the test of whether an RBAC model was built correctly at the start.

RBACSecurityCustomer PortalArchitecture
Building a Customer Portal With Role-Based Access — Wholly Software