Nest Authbeta

RBAC — Roles & Permissions

Role-based access control with multiple guards (web, api, mobile).

Nest Auth ships a full role-and-permission system. You can run multiple parallel role hierarchies — for example, "web roles" for browser users and "api roles" for service accounts — and check them per route.

Concepts

ThingWhat it is
RoleNamed bucket — admin, editor, member. Lives in nest_auth_roles.
PermissionFine-grained capability — orders.read, orders.write. Lives in nest_auth_permissions.
Role-permissionMany-to-many between the two.
GuardA namespace for roles. 'web', 'api', 'mobile' — you decide.
User-tenant accessA user's role(s) within a tenant, on a specific guard.

Configuring guards

Tell the module which guard namespaces exist:

NestAuthModule.forRoot({
  // …
  roleGuards: ['web', 'api', 'mobile'],     // default: ['web']
});

A role row carries its guard, so an admin web role and an admin api role are independent records that don't conflict.

When you need multiple guards

The minimum useful set is one. Reach for multiple guards when the same backend serves several frontends that should authenticate independently — typically:

GuardFrontendRoles you'd find here
ADMINInternal admin console (separate origin)super_admin, support, ops
PORTALTenant-facing SaaS appowner, admin, member, billing
FRONTENDMobile app (or public marketing site)customer, guest

A user can have roles on more than one guard — Alice could be admin on PORTAL and support on ADMIN. The library doesn't care; your loginHooks.onLogin decides which guard the user is allowed to log into based on the request's origin (or a header). See the multi-platform login recipe for the full pattern.

Creating roles per guard

import { Injectable } from '@nestjs/common';
import { RoleService } from '@ackplus/nest-auth';
 
@Injectable()
export class RoleSeederService {
  constructor(private readonly roles: RoleService) {}
 
  async seed() {
    // Same name, different guard, different row.
    await this.roles.create({ name: 'admin',  guard: 'ADMIN',  isSystem: true, permissions: ['*'] });
    await this.roles.create({ name: 'admin',  guard: 'PORTAL', isSystem: true, permissions: ['orders.*', 'users.*'] });
    await this.roles.create({ name: 'member', guard: 'PORTAL', isSystem: true, permissions: ['orders.read'] });
    await this.roles.create({ name: 'customer', guard: 'FRONTEND', isSystem: true, permissions: ['profile.read', 'profile.write'] });
  }
}

A worked seeder migration with idempotent upserts: seeding-roles-and-permissions recipe.

Decorators

import {
  Auth,
  Public,
  NestAuthRoles,
  NestAuthPermissions,
} from '@ackplus/nest-auth';
 
@Auth()                                          // requires authentication
@NestAuthRoles(['admin', 'editor'])              // ANY of these roles (default)
@NestAuthRoles(['admin', 'finance'], 'api')      // explicitly target the 'api' guard
@NestAuthPermissions(['orders.read'])            // requires the permission
@NestAuthPermissions(['orders.read', 'orders.write'], true)  // ALL of these (matchAll)
@Public()                                        // bypass auth entirely

@Auth(true) makes auth optional — if the user is logged in, the guard populates the request context; if not, the route still runs.

@SkipMfa() lets a route bypass MFA enforcement (useful for the /auth/mfa/verify endpoint itself).

Checking from the client

The client and React layer ship matching utilities:

// Vanilla JS
import { hasRole, hasPermission } from '@ackplus/nest-auth-client';
 
if (hasRole(user, 'admin')) { … }
if (hasPermission(user, ['orders.read', 'orders.write'], true)) { … }
// React
import { useHasRole, useHasPermission } from '@ackplus/nest-auth-react';
 
const isAdmin = useHasRole('admin');
const canEdit = useHasPermission(['orders.read', 'orders.write'], true);

Or use guard components:

import { RequireRole, RequirePermission } from '@ackplus/nest-auth-react';
 
<RequireRole role="admin">
  <AdminPanel />
</RequireRole>
 
<RequirePermission permission={['orders.read', 'orders.write']} matchAll>
  <OrderEditor />
</RequirePermission>

Where roles & permissions are evaluated

The default flow:

  1. User logs in. Server reads their nest_auth_user_accesses row for the active tenant.
  2. Server resolves their roles, then expands roles → permissions via nest_auth_role_permissions.
  3. The full roles[] and permissions[] arrays are baked into the session payload (and thus into useUser()).
  4. Guards on protected routes check against those arrays — no per-request DB hit.

External role systems (Okta / Auth0 / custom IDP)

Don't want to store roles in your database? Replace the resolution step with a hook:

authorization: {
  async resolveRoles(user) {
    const groups = await okta.getGroups(user.metadata.oktaId);
    return groups.map(g => g.name);
  },
  async resolvePermissions(user, roles) {
    return rolesToPermissions(roles);
  },
},

Both hooks run on every login and refresh. See the external role resolver recipe for a full example.

Assigning roles to users

Roles aren't useful until you attach them to a user × tenant pair. That mapping lives on NestAuthUserAccess (or NestAuthPlatformAccess for staff). The most common spot to assign a default role is the registrationHooks.onSignup hook — it runs after the user is created but before the first session is built, so the role lands in the very first JWT.

import { NestAuthRole, RoleNameEnum, RoleGuardEnum } from '@ackplus/nest-auth';
 
registrationHooks: {
  onSignup: async (user, input) => {
    if (!input?.tenantId) return;
 
    const access = await user.getUserAccess(input.tenantId, /* createIfMissing */ true);
 
    const memberRole = await NestAuthRole.findOne({
      where: { name: RoleNameEnum.MEMBER, guard: RoleGuardEnum.PORTAL, isSystem: true },
      select: ['id'],
    });
 
    if (memberRole) {
      await access.assignRoles([memberRole.id]);
    }
  },
},

For programmatic role/permission management from your own services (admin endpoints, invitation flows, role-change UIs), use RoleService and PermissionService. They cover create / update / delete / assign / list, emit the matching events, and respect isSystem protection.

System vs custom roles

Roles can be marked isSystem: true. Those are protected — the admin console won't let users delete or rename them, and the library treats them as canonical. Use this for the handful of roles your code references by name (admin, member, owner); leave isSystem: false for end-user-defined roles ("Marketing Lead", "QA Manager") that admins can edit freely.

On this page