DEV Community

Himanshu Gupta
Himanshu Gupta

Posted on

Stop Assigning Permissions to Users. Build RBAC the Right Way

Every application starts the same way.

You launch your MVP with just one role:

  • Admin

A few weeks later, someone asks for an Editor.

Then comes:

  • Moderator
  • Manager
  • HR
  • Customer Support
  • Sales
  • Finance
  • Super Admin

Suddenly, your authorization logic looks like this:

if(user.isAdmin){
   ...
}

if(user.isManager){
   ...
}

if(user.department == "HR"){
   ...
}

if(user.role == "Finance"){
   ...
}
Enter fullscreen mode Exit fullscreen mode

A few months later, the codebase becomes impossible to maintain.

Sound familiar?

This is exactly the problem Role-Based Access Control (RBAC) was designed to solve.


Why Direct Permissions Don't Scale

A common beginner approach is assigning permissions directly to users.

John

✓ Create User
✓ Delete User
✓ Edit User
✓ View Reports
✓ Create Posts
✓ Delete Posts
Enter fullscreen mode Exit fullscreen mode

Now imagine your company has 50,000 users.

Changing one permission means updating thousands of records.

That's not scalable.

Instead, RBAC introduces one extra layer:

User
   ↓
Role
   ↓
Permissions
Enter fullscreen mode Exit fullscreen mode

That single abstraction changes everything.


The Core Idea Behind RBAC

Instead of asking:

What permissions does this user have?

RBAC asks:

Which role does this user belong to?

Roles then define permissions.

For example:

Admin
 ├── Create Users
 ├── Delete Users
 ├── Edit Users
 └── Manage Roles

Editor
 ├── Create Posts
 ├── Edit Posts
 └── Publish Posts

Viewer
 └── Read Posts
Enter fullscreen mode Exit fullscreen mode

If tomorrow every Editor needs a new permission...

You update one role, not thousands of users.


The RBAC Database Design

A production-ready RBAC system usually consists of five tables.

Users

Roles

Permissions

User_Roles

Role_Permissions
Enter fullscreen mode Exit fullscreen mode

The two pivot tables are what make RBAC flexible.


Users

id
name
email
Enter fullscreen mode Exit fullscreen mode

Roles

id
name

Admin
Editor
Manager
HR
Enter fullscreen mode Exit fullscreen mode

Permissions

id
name

users.create
users.update
users.delete
posts.publish
reports.view
Enter fullscreen mode Exit fullscreen mode

Notice the naming convention:

resource.action
Enter fullscreen mode Exit fullscreen mode

This keeps permissions organized and predictable.


Many-to-Many Relationships

One user can have multiple roles.

John

↓

Admin

Manager
Enter fullscreen mode Exit fullscreen mode

Likewise, one role can have many permissions.

Manager

↓

View Reports

Approve Leave

Assign Tasks
Enter fullscreen mode Exit fullscreen mode

Database-wise:

Users

↓

User_Roles

↓

Roles

↓

Role_Permissions

↓

Permissions
Enter fullscreen mode Exit fullscreen mode

This design eliminates duplication while remaining highly flexible.


Permission Resolution Flow

When a request arrives, the authorization flow typically looks like this:

User Request
      │
Authenticate User
      │
Load User Roles
      │
Load Role Permissions
      │
Merge Permissions
      │
Permission Exists?
      │
 YES ─────────→ Allow
 NO  ─────────→ Deny
Enter fullscreen mode Exit fullscreen mode

The application never checks roles directly.

It checks permissions.

That's an important distinction.


Roles Should Never Exist in Business Logic

One mistake I see often is this:

if($user->role == "Admin"){
    // allow
}
Enter fullscreen mode Exit fullscreen mode

The problem?

What happens when a new role called Super Admin is introduced?

Or Regional Manager?

Now every condition must be updated.

Instead:

$user->can('users.delete')
Enter fullscreen mode Exit fullscreen mode

Now the application doesn't care which role grants the permission.

It only cares whether the permission exists.

This follows the Principle of Least Privilege and keeps your code extensible.


Why Pivot Tables Matter

Many developers ask:

Why not store permissions as JSON?

Because permissions change frequently.

Using pivot tables gives you:

  • Efficient joins
  • Referential integrity
  • Easy permission updates
  • Better indexing
  • Cleaner queries

Most importantly, it keeps your data normalized.


Caching Is Essential

Imagine every API request executes:

SELECT roles...

SELECT permissions...

JOIN role_permissions...
Enter fullscreen mode Exit fullscreen mode

That quickly becomes expensive.

Instead, cache the final permission set immediately after authentication.

User Login

↓

Load Permissions

↓

Store in Redis

↓

Future Requests

↓

Redis Lookup
Enter fullscreen mode Exit fullscreen mode

This eliminates unnecessary database queries and significantly improves performance.


RBAC with JWT Authentication

In stateless APIs, RBAC works seamlessly with JWT.

A typical flow is:

Login
   │
Generate JWT
   │
Load Roles
   │
Load Permissions
   │
Cache Permission Set
   │
Return Token
Enter fullscreen mode Exit fullscreen mode

On each request:

JWT Validation
       │
Redis Cache
       │
Permission Check
Enter fullscreen mode Exit fullscreen mode

This approach minimizes latency while keeping authorization centralized.


RBAC vs ABAC

RBAC isn't the only authorization model.

Another popular approach is Attribute-Based Access Control (ABAC).

RBAC

Manager

↓

Approve Expense
Enter fullscreen mode Exit fullscreen mode

ABAC

Department == Finance

AND

Expense < $5000

AND

Business Hours
Enter fullscreen mode Exit fullscreen mode

RBAC answers:

Who are you?

ABAC answers:

What are the current conditions?

Many enterprise systems combine both models.


Production Best Practices

A scalable RBAC implementation should follow these principles:

  • Never assign permissions directly to users.
  • Always use pivot tables for many-to-many relationships.
  • Follow a consistent resource.action naming convention.
  • Cache permissions after authentication.
  • Index pivot tables for faster joins.
  • Check permissions instead of roles in application code.
  • Keep authorization logic centralized in middleware or policies.

Popular RBAC Libraries

Most frameworks provide mature RBAC solutions.

Laravel

  • Spatie Laravel Permission

Spring Boot

  • Spring Security

ASP.NET

  • ASP.NET Identity

Node.js

  • CASL
  • AccessControl

These libraries implement the same architecture discussed above while handling caching, middleware, and authorization helpers for you.


Common Mistakes

❌ Assigning permissions directly to users

❌ Hardcoding role names in business logic

❌ Skipping permission caching

❌ Using comma-separated permission strings

❌ Storing permissions inside JWT tokens forever without refresh

❌ Ignoring database indexes on pivot tables

Avoiding these mistakes early can save countless hours as your application grows.


Final Thoughts

Role-Based Access Control isn't just about restricting access—it's about building an authorization system that remains maintainable as your application evolves.

The biggest shift in mindset is this:

Users shouldn't own permissions. Roles should.

By introducing a simple layer of abstraction, RBAC makes permission management cleaner, more scalable, and easier to extend.

Whether you're building a small SaaS product or an enterprise platform with millions of users, understanding RBAC fundamentals will help you design secure and maintainable applications.


What do you prefer for large-scale systems—pure RBAC, ABAC, or a hybrid approach? Share your experience in the comments!

#systemdesign #backend #rbac #security #authorization #softwarearchitecture #laravel #nodejs #java #developers

Top comments (0)