RBAC (Role-Based Access Control) Implementation for Web Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing RBAC (Role-Based Access Control) for Web Applications

A user logs in and sees exactly what they're authorized to see—no more, no less. Sounds trivial until you're counting: 12 user types, 40 interface sections, a permissions matrix on an A3 sheet you need to maintain in code. RBAC is the standard answer: rights are assigned not to users directly but to roles, and users receive roles.

Data Model

Minimal schema for PostgreSQL:

CREATE TABLE roles (
    id          SERIAL PRIMARY KEY,
    name        VARCHAR(64) NOT NULL UNIQUE,  -- 'admin', 'editor', 'viewer'
    description TEXT
);

CREATE TABLE permissions (
    id       SERIAL PRIMARY KEY,
    resource VARCHAR(128) NOT NULL,  -- 'articles', 'users', 'reports'
    action   VARCHAR(64)  NOT NULL,  -- 'create', 'read', 'update', 'delete', 'publish'
    UNIQUE (resource, action)
);

CREATE TABLE role_permissions (
    role_id       INT REFERENCES roles(id)       ON DELETE CASCADE,
    permission_id INT REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
    user_id INT REFERENCES users(id) ON DELETE CASCADE,
    role_id INT REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

This is classic RBAC0 implementation. For hierarchical roles (admin inherits all editor permissions), add role_hierarchy table with parent_role_id / child_role_id and use recursive CTE for checks.

Permission Checking on Backend

Node.js + Express, middleware approach:

// permissions.js—load permissions from DB on startup or cache in Redis
async function loadUserPermissions(userId) {
  const rows = await db.query(`
    SELECT DISTINCT p.resource, p.action
    FROM user_roles ur
    JOIN role_permissions rp ON rp.role_id = ur.role_id
    JOIN permissions p ON p.id = rp.permission_id
    WHERE ur.user_id = $1
  `, [userId]);

  return new Set(rows.map(r => `${r.resource}:${r.action}`));
}

// middleware/can.js
function can(resource, action) {
  return async (req, res, next) => {
    const perms = await loadUserPermissions(req.user.id);
    if (perms.has(`${resource}:${action}`)) {
      return next();
    }
    res.status(403).json({ error: 'Forbidden' });
  };
}

// routes
router.delete('/articles/:id', authenticate, can('articles', 'delete'), deleteArticle);
router.post('/articles',       authenticate, can('articles', 'create'), createArticle);

For PHP/Laravel the pattern is similar—Gate and Policy:

// AuthServiceProvider
Gate::before(function (User $user, string $ability) {
    if ($user->hasRole('superadmin')) {
        return true; // superadmin bypasses all checks
    }
});

Gate::define('articles.delete', function (User $user) {
    return $user->hasPermission('articles', 'delete');
});

// In controller
public function destroy(Article $article)
{
    $this->authorize('articles.delete');
    $article->delete();
    return response()->noContent();
}

The hasPermission method makes one query with JOIN or gets from cache—depends on load.

Caching Permission Matrix

Running a three-table JOIN on every HTTP request is wasteful. User permissions change rarely—this caches well:

// redis cache, TTL 5 minutes
async function getUserPermissions(userId) {
  const cacheKey = `user_perms:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return new Set(JSON.parse(cached));

  const perms = await loadUserPermissions(userId);
  await redis.setex(cacheKey, 300, JSON.stringify([...perms]));
  return perms;
}

// Invalidate when user roles change
async function assignRole(userId, roleId) {
  await db.query(
    'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
    [userId, roleId]
  );
  await redis.del(`user_perms:${userId}`);
}

Role Management in UI

Administrators need to see and edit the matrix. Minimal API:

GET    /api/roles                    — list roles
POST   /api/roles                    — create role
GET    /api/roles/:id/permissions    — role permissions
PUT    /api/roles/:id/permissions    — update role permissions (array of permission_ids)
GET    /api/users/:id/roles          — user roles
POST   /api/users/:id/roles          — assign role
DELETE /api/users/:id/roles/:roleId  — unassign role

Frontend—table with checkboxes resource × action—standard UI for this.

What Affects Timeline

Basic implementation (3–5 roles, no inheritance, no role management UI)—2–3 days. Schema, middleware, integration with existing auth, tests.

Add role/permission management UI—add 2 days. Hierarchical roles or multi-tenancy (roles isolated per organization)—add 2–3 days for schema and logic complexity.