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

Building Robust Role-Based Access Control (RBAC) Systems

Building Robust Role-Based Access Control (RBAC) Systems

Authentication tells you who someone is. Authorization decides what they're allowed to do once they're in — and that gets messy fast if adding a new role means shipping a code change. Here's an RBAC setup that avoids that.

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)

// 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:

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:

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:

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:

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:

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:

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.

Wrapping Up

String-based permissions if you want flexibility, bitmasks if you want speed — most apps never need both. Either way, cache the resolved permission set; roles change rarely but get checked on nearly every request.

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

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Writes about backend systems and databases based on what comes up while building with Node.js, MongoDB, PostgreSQL, and Redis.

You May Also Like

Designing a Role-Based Access Control (RBAC) System from Scratch
Backend Development
2 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
4 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
4 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