Learn how to add custom roles to your web app easily with our step-by-step guide for better user management and security.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Custom Roles Matter
Remember the days when user permissions were simply "admin" or "user"? Those binary days are long gone. Modern web applications need nuanced access control where marketing teams can access analytics but not billing, and support staff can help customers without seeing sensitive data.
Custom roles are the foundation of a secure, scalable permission system that aligns with how real organizations actually operate. They're not just a technical feature—they're a business enabler.
Before writing a single line of code, consider these essentials:
Let's break this down into practical steps with real code examples:
You'll typically need three tables:
-- Users table (you likely already have this)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
-- other user fields
);
-- Roles table
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(255)
);
-- Permissions table
CREATE TABLE permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(255)
);
-- Role-Permission junction table
CREATE TABLE role_permissions (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);
-- User-Role junction table
CREATE TABLE user_roles (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
Here's how to implement role checking in different frameworks:
Node.js with Express:
// middleware/auth.js
const checkPermission = (requiredPermission) => {
return async (req, res, next) => {
try {
// Assuming you store user info in req.user after authentication
const userId = req.user.id;
// Query to check if user has the permission (through their roles)
const hasPermission = await db.query(`
SELECT 1 FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = ? AND p.name = ?
LIMIT 1
`, [userId, requiredPermission]);
if (hasPermission.length > 0) {
return next(); // User has permission, proceed
}
return res.status(403).json({ error: "Access denied" });
} catch (error) {
return res.status(500).json({ error: "Server error" });
}
};
};
// Usage in routes
// routes/products.js
const express = require('express');
const router = express.Router();
const { checkPermission } = require('../middleware/auth');
router.post('/products', checkPermission('create_product'), (req, res) => {
// Handle creating a product
});
router.delete('/products/:id', checkPermission('delete_product'), (req, res) => {
// Handle deleting a product
});
module.exports = router;
PHP Laravel:
// Create a Gate in AuthServiceProvider.php
use Illuminate\Support\Facades\Gate;
public function boot()
{
$this->registerPolicies();
// Define a dynamic gate that checks permissions
Gate::define('permission', function ($user, $permission) {
return $user->hasPermission($permission);
});
}
// Add methods to your User model
// User.php
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles');
}
public function hasPermission($permission)
{
foreach ($this->roles as $role) {
if ($role->permissions->contains('name', $permission)) {
return true;
}
}
return false;
}
// In your controller
public function deleteProduct(Product $product)
{
if (Gate::denies('permission', 'delete_product')) {
abort(403);
}
// Delete product logic
}
// Or use the @can directive in Blade
@can('permission', 'edit_product')
<a href="/products/{{ $product->id }}/edit">Edit</a>
@endcan
Python Django:
# permissions.py
from functools import wraps
from django.core.exceptions import PermissionDenied
from django.db.models import Q
def has_permission(user, permission_name):
# Check if user has the permission through any of their roles
return user.roles.filter(
permissions__name=permission_name
).exists()
def permission_required(permission_name):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if has_permission(request.user, permission_name):
return view_func(request, *args, **kwargs)
raise PermissionDenied
return _wrapped_view
return decorator
# Usage in views
# views.py
from .permissions import permission_required
@permission_required('create_product')
def create_product(request):
# Create product logic
pass
The frontend needs to adapt based on user permissions:
React Example:
// components/PermissionGate.jsx
import React, { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';
const PermissionGate = ({ permission, children }) => {
const { user } = useContext(AuthContext);
// Check if user has permission
const hasPermission = user?.permissions?.includes(permission);
// Render children only if user has permission
return hasPermission ? children : null;
};
// Usage in components
import PermissionGate from './PermissionGate';
function ProductsPage() {
return (
<div>
<h1>Products</h1>
<PermissionGate permission="view_products">
{/* Product listing UI */}
</PermissionGate>
<PermissionGate permission="create_product">
<button>Add New Product</button>
</PermissionGate>
</div>
);
}
Don't forget to build an interface for administrators to manage roles:
Don't skip this crucial part:
// Example Jest test for Express middleware
test('checkPermission middleware denies access without permission', async () => {
// Mock user without required permission
const req = {
user: { id: 1 }
};
// Mock database response (no permissions)
db.query = jest.fn().mockResolvedValue([]);
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
const next = jest.fn();
// Call middleware
await checkPermission('edit_product')(req, res, next);
// Verify it denied access
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
});
As your application grows:
A well-designed role system grows with your business. What begins as a simple permission setup should be architected to evolve into fine-grained access control without requiring a complete rewrite.
The code I've shown isn't just about technical implementation—it's about translating organizational structures and business requirements into secure, manageable access controls that both protect your data and empower your users to do their jobs effectively.
Remember: your role system is the digital reflection of how responsibility and trust are distributed in your organization. Build it with the same care you'd use to design your actual org chart.
Explore the top 3 practical use cases for adding custom roles to enhance your web app’s functionality.
Create granular permission structures aligned with data sensitivity classifications, allowing organizations to enforce precise access boundaries. This prevents the common "all-or-nothing" approach where employees either get excessive permissions or lack necessary access to perform their jobs effectively.
Define role templates tailored to specific business processes rather than generic system-wide roles. This creates natural boundaries that match how your organization actually functions, reducing both security risks and administrative overhead.
Create time-bound custom roles for cross-functional initiatives that require temporary elevation of permissions. This maintains security while enabling project teams to move efficiently without constantly requesting access.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â