Skip to main content
PensionsPortal.ie enforces a role-based access control (RBAC) model with four roles and mandatory tenant scoping. Every API route checks both the user’s role and their brokerId / employerId before returning or mutating data.

Role Definitions

Role (DB)Session RoleDescription
SuperAdminadminFull platform access. No broker or employer scope restriction. Reserved for platform operators.
BrokerAdminbrokerFull access within their brokerId. Manages schemes, members, compliance tasks for their clients.
BrokerUserbrokerSame data scope as BrokerAdmin. UI may restrict specific administrative actions.
TrusteeemployerScoped to their employerId. Can view scheme data and submit required information.
member role is defined in the type system for future member-portal use but is not currently active in the application UI.

Tenant Scoping

Multi-tenancy is enforced structurally, not just by convention. Every database query that returns scheme, member, or employer data includes a brokerId or employerId filter derived from the authenticated session:
// Example: broker can only see schemes belonging to their brokerId
const schemes = await db
  .select()
  .from(schemesTable)
  .where(eq(schemesTable.brokerId, session.user.brokerId))
A broker user cannot access another broker’s data, even if they know the scheme or member ID — the WHERE clause makes cross-tenant access structurally impossible at the query level.

Role Mapping

The application maps database role names to session role values in src/lib/auth.ts:
const roleMap: Record<string, UserRole> = {
  SuperAdmin:  "admin",
  BrokerAdmin: "broker",
  BrokerUser:  "broker",
  Trustee:     "employer",
}
This mapping is applied at login and embedded in the JWT. Changes to a user’s role in the database take effect at next login.

Session Claims

Every authenticated session carries:
interface PensionsUser {
  id: string
  email: string
  role: UserRole        // "admin" | "broker" | "employer" | "member"
  brokerId: string | null
  employerId: string | null
}
These claims are used by API route handlers and React Server Components to gate access.

API Enforcement Pattern

1

Extract session

const session = await auth() — retrieves the validated JWT session server-side.
2

Check authentication

If !session?.user, return 401 Unauthorized.
3

Check role

If the route requires a specific role (e.g., broker or admin), check session.user.role. Return 403 Forbidden if insufficient.
4

Scope the query

Append brokerId or employerId to every database query using the session claim.

Audit Trail

All material actions — regardless of role — are written to the append-only audit_logs table, recording the actorId, actorType, action performed, previous state, and new state. See Audit Logging for schema details.

AI Change Proposals

AI-generated suggestions never bypass RBAC. The changeProposals table enforces:
  • Proposals are scoped to a brokerId (and optionally schemeId, memberId)
  • Only an authorised broker user can approve or reject a proposal
  • Required attestations must be completed before approval is permitted
  • Proposals auto-expire if not acted upon
This ensures AI capability cannot exceed human-authorised permissions.