Bablu Kumar Singh
Back to Blog
Backend Development
6 min read
May 24, 2026

Building Robust Role-Based Access Control (RBAC) Systems

Building Robust Role-Based Access Control (RBAC) Systems

Authorization is the gatekeeper of your application. Authentication verifies *who* you are; authorization determines *what* you can do. A well-designed Role-Based Access Control (RBAC) system keeps unauthorized users out while remaining flexible enough to accommodate new roles without code changes.

The Permission Model

At the foundation of RBAC are three entities: Users, Roles, and Permissions. The relationships are:

  • A User is assigned one or more Roles.
  • A Role contains a set of Permissions.
  • A Permission represents a granular action (e.g., users:read, orders:delete).

Schema Design (MongoDB)

typescript Code Block
// Permission embedded in Role for fast lookups
const RoleSchema = new mongoose.Schema({
  name: { type: String, required: true, unique: true },
  description: { type: String },
  permissions: [{ type: String }], // e.g., ['users:read', 'users:write', 'orders:read']
});

const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  passwordHash: { type: String, required: true },
  roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }],
});

Embedding permissions as strings in the Role document means a single populate() call gives you everything needed to authorize a request — no joins, no extra queries.

Bitmask Approach for High Performance

For systems with a fixed, manageable set of permissions, bitmask encoding delivers the fastest possible permission checks:

typescript Code Block
const Permissions = {
  USERS_READ:    1 << 0,  // 1
  USERS_WRITE:   1 << 1,  // 2
  USERS_DELETE:  1 << 2,  // 4
  ORDERS_READ:   1 << 3,  // 8
  ORDERS_WRITE:  1 << 4,  // 16
  ORDERS_DELETE: 1 << 5,  // 32
  ADMIN_PANEL:   1 << 6,  // 64
} as const;

// Define roles as bitmask sums
const ROLES = {
  viewer:  Permissions.USERS_READ | Permissions.ORDERS_READ,                    // 9
  editor:  Permissions.USERS_READ | Permissions.USERS_WRITE |
           Permissions.ORDERS_READ | Permissions.ORDERS_WRITE,                  // 27
  admin:   Object.values(Permissions).reduce((a, b) => a | b, 0),              // 127
};

Permission check is a single bitwise AND — executed in nanoseconds:

typescript Code Block
function hasPermission(roleMask: number, permission: number): boolean {
  return (roleMask & permission) === permission;
}

hasPermission(ROLES.editor, Permissions.ORDERS_WRITE);  // true
hasPermission(ROLES.viewer, Permissions.USERS_DELETE);   // false

Authorization Middleware

Create a reusable middleware factory that accepts required permissions:

typescript Code Block
import { Request, Response, NextFunction } from 'express';

export function authorize(...requiredPermissions: string[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = req.user;
    if (!user) return res.status(401).json({ error: 'Authentication required' });

    // Gather all permissions from user's roles
    const userPermissions = new Set(
      user.roles.flatMap((role: any) => role.permissions)
    );

    const hasAll = requiredPermissions.every((p) => userPermissions.has(p));
    if (!hasAll) {
      return res.status(403).json({
        error: 'Forbidden',
        required: requiredPermissions,
      });
    }

    next();
  };
}

Usage on routes:

typescript Code Block
router.get('/users', authorize('users:read'), usersController.list);
router.delete('/users/:id', authorize('users:delete'), usersController.remove);
router.post('/orders', authorize('orders:write'), ordersController.create);

Hierarchical Roles

In many organizations, a Manager should inherit all permissions of a Viewer, and an Admin should inherit all permissions of a Manager. Model this with a parent reference:

typescript Code Block
const RoleSchema = new mongoose.Schema({
  name: { type: String, required: true },
  permissions: [{ type: String }],
  parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Role', default: null },
});

async function getEffectivePermissions(roleId: string): Promise<string[]> {
  const role = await Role.findById(roleId).populate('parent');
  let permissions = [...role.permissions];
  if (role.parent) {
    const parentPerms = await getEffectivePermissions(role.parent._id);
    permissions = [...new Set([...permissions, ...parentPerms])];
  }
  return permissions;
}

Cache the resolved permission set in Redis so hierarchical lookups do not hit the database on every request.

Caching Role Data

Roles change infrequently, making them ideal cache candidates:

typescript Code Block
async function getUserPermissions(userId: string): Promise<string[]> {
  const cacheKey = `perms:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await User.findById(userId).populate('roles');
  const permissions = user.roles.flatMap((r: any) => r.permissions);
  await redis.set(cacheKey, JSON.stringify(permissions), 'EX', 3600);
  return permissions;
}

Invalidate the cache whenever a role is updated or a user's role assignment changes.

Key Takeaways

  • Separate authentication from authorization — different concerns, different middleware.
  • Use string-based permissions for flexibility or bitmasks for raw speed.
  • Cache resolved permissions in Redis — roles change rarely but are checked on every request.
  • Model hierarchical roles with parent references and recursive resolution.
  • Keep the authorization middleware generic and reusable — never hard-code role names in controllers.

A well-implemented RBAC system is invisible to your users and invaluable to your security posture.

#RBAC#Security#Authorization#Express.js
Bablu Kumar Singh
Written by

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Backend-Focused Full Stack Developer specializing in Node.js, MongoDB, PostgreSQL, Redis, RabbitMQ, AWS, Docker, System Design, and React Native.

You May Also Like

Designing a Role-Based Access Control (RBAC) System from Scratch
Backend Development
5 min read

Designing a Role-Based Access Control (RBAC) System from Scratch

A blueprint on implementing hierarchical permission matrices in Express.js middleware using bitwise operations and MongoDB caching.

May 24, 2026Read
Node.js API Design Best Practices
Backend Development
7 min read

Node.js API Design Best Practices

A comprehensive guide to designing clean, scalable, and maintainable REST APIs with Node.js and Express.js — covering project structure, validation, error handling, versioning, and security.

May 2, 2026Read
Authentication Systems in Node.js: JWT, Refresh Tokens, and Security
Backend Development
7 min read

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security

Build a production-grade authentication system in Node.js — covering JWT access tokens, refresh token rotation, password hashing with bcrypt, and common security pitfalls to avoid.

May 28, 2026Read