How DeskcommCRM Implements Role-Based Access Control (RBAC)

DeskcommCRM enforces Role-Based Access Control (RBAC) through a centralized requireRole guard in lib/auth/require-role.ts that validates JWT tokens, resolves tenant context, fetches effective roles from a Supabase RPC, and enforces numeric rank comparisons with mandatory MFA checks.

DeskcommCRM is an open-source customer relationship management platform built on Next.js and Supabase. Its authorization architecture relies on a strict, auditable Role-Based Access Control (RBAC) system that centralizes permission logic in a single validation layer. Every protected API route invokes this guard to ensure users possess the minimum required privileges before accessing sensitive data or operations.

The Central Authorization Guard: requireRole

The heart of DeskcommCRM's RBAC implementation lives in lib/auth/require-role.ts. This module exports the requireRole function, which serves as the exclusive entry point for access control decisions across the entire application. Rather than scattering permission checks throughout business logic, the platform consolidates all authorization concerns—including authentication verification, tenant resolution, role fetching, and multi-factor authentication (MFA) validation—within this single utility.

The Eight-Step Authorization Flow

When an API route invokes requireRole, the function executes a rigorous, sequential validation pipeline:

  1. Authentication Verification – The loadAuthUser() helper validates the JWT token using Supabase's auth.getUser() method (never getSession()). Invalid or missing tokens immediately return a 401 Unauthenticated response.

  2. Tenant Resolution – resolveActiveOrg(user) extracts the active organization from a trusted, cookie-backed membership list. If no valid tenant context exists, the guard returns a 403 Forbidden-Tenant error.

  3. Effective Role Retrieval – The system queries the database RPC fn_user_role_in_org(p_org), a PostgreSQL function marked as SECURITY DEFINER that serves as the single source of truth for role data. This function is used by both the RBAC guard and Row Level Security (RLS) policies.

  4. Rank Translation – The ROLE_RANK mapping (defined in lib/auth/types.ts) converts role strings (e.g., admin, manager, viewer) into numeric values, enabling simple mathematical privilege comparisons.

  5. Platform-Admin Bypass – If the allowPlatformAdmin option is enabled and user.is_platform_admin is true, the function skips standard rank checks, granting cross-tenant superuser access.

  6. MFA Enforcement – Before final authorization, await mfaEmDivida() verifies multi-factor authentication requirements. Missing MFA triggers a 403 MFA-Required error and logs an audit event.

  7. Privilege Comparison – The guard compares the user's effective rank against ROLE_RANK[min]. If the user's rank is lower, fail("forbidden_role") from lib/api/wrappers.ts returns a structured 403 response and emits an authz.denied audit entry.

  8. Success Resolution – Upon passing all checks, the function returns { ok: true, user, org }, where org.role contains the effective role fetched directly from the database.

Role Hierarchy and Rank-Based Comparisons

DeskcommCRM eschews complex permission matrices in favor of a rank-based hierarchy defined in lib/auth/types.ts. The ROLE_RANK constant maps each role to a numeric value (e.g., admin = 3, manager = 2, viewer = 1). This design allows the authorization logic to use simple "greater-than-or-equal" comparisons rather than bitwise operations or complex set intersections.

This approach ensures that higher-privileged roles automatically inherit permissions granted to lower ranks, simplifying both the mental model and the implementation code.

Multi-Factor Authentication Integration

Security hardening extends beyond role verification. The requireRole guard enforces MFA-first policies by validating multi-factor authentication before evaluating role ranks. This sequencing prevents role information leakage to unauthenticated sessions and ensures that privilege escalation attempts are blocked at the authentication layer.

The mfaEmDivida() check integrates with the platform's audit system, logging denied attempts asynchronously to avoid impacting response latency while maintaining comprehensive security trails.

Platform Administration Overrides

For global system administration, DeskcommCRM supports a platform-admin bypass mechanism. When requireRole is called with allowPlatformAdmin: true, the guard checks the user.is_platform_admin boolean flag. Platform administrators bypass tenant-specific rank checks, enabling them to access organization-scoped endpoints without traditional membership constraints.

This capability is critical for support operations and global metrics collection while maintaining strict separation between standard user authorization and superuser privileges.

Usage Examples

Protecting Standard API Routes

API routes import the guard and specify the minimum required role. The function returns a standardized response object that routes can immediately return to clients:

// app/api/v1/customers/create.ts
import { requireRole } from "@/lib/auth/require-role";
import { ok } from "@/lib/api/wrappers";

export async function POST(req: Request) {
  // Require at least "manager" role for this endpoint
  const authz = await requireRole("manager", { 
    requestId: req.headers.get("x-request-id") 
  });
  
  if (!authz.ok) return authz.response;   // 401 / 403 automatically handled

  // ...handle the POST body, perform DB writes, etc.
  return ok({ message: "Cliente criado com sucesso." });
}

Leveraging Platform-Admin Privileges

Global endpoints utilize the bypass option to grant access to platform administrators:

// app/api/v1/platform/metrics.ts
import { requireRole } from "@/lib/auth/require-role";

export async function GET(req: Request) {
  // Platform admins can access this endpoint without a tenant context
  const authz = await requireRole("viewer", { allowPlatformAdmin: true });
  if (!authz.ok) return authz.response;

  // ...return global metrics
}

Inspecting Authorization Results

Successful authorization returns rich context about the user and their organizational role:

const { ok, user, org } = await requireRole("admin");
if (ok) {
  console.log("User ID:", user.id);
  console.log("Active org:", org.orgId);
  console.log("Effective role:", org.role); // role fetched from DB
}

Database-Level Consistency

The RBAC system maintains consistency between application logic and database policies through the fn_user_role_in_org(p_org) RPC. Defined as a SECURITY DEFINER function in Supabase migrations, this PostgreSQL routine calculates the user's effective role within a specific organization. Because both the requireRole guard and Supabase RLS policies invoke this identical function, the platform eliminates authorization drift between application layers and database constraints.

This architectural decision ensures that direct database access (through Supabase clients or SQL) respects the same role hierarchy as the Next.js API routes, creating a unified security perimeter.

Summary

  • DeskcommCRM centralizes RBAC logic in the requireRole function within lib/auth/require-role.ts, ensuring consistent authorization across all API routes.
  • Rank-based hierarchy via ROLE_RANK in lib/auth/types.ts enables simple numeric comparisons for privilege evaluation.
  • Multi-factor authentication is enforced before role checks via mfaEmDivida(), preventing information leakage and strengthening security posture.
  • Platform administrators can bypass tenant restrictions using the allowPlatformAdmin option for global system management.
  • Database consistency is achieved by sharing the fn_user_role_in_org RPC between the application guard and PostgreSQL RLS policies.
  • Comprehensive auditing logs all authorization denials through the fail wrapper in lib/api/wrappers.ts without blocking response threads.

Frequently Asked Questions

How does DeskcommCRM prevent unauthorized access across different organizations?

The resolveActiveOrg function in lib/auth/server.ts extracts the active tenant from a cryptographically signed cookie-backed membership list. If the user lacks a valid membership for the requested organization, the guard returns a 403 Forbidden-Tenant error before evaluating role permissions. This ensures strict multi-tenancy isolation at the authentication layer.

Why does the RBAC system use numeric ranks instead of permission lists?

DeskcommCRM implements a rank-based hierarchy through the ROLE_RANK mapping to simplify privilege evaluation. By assigning numeric values to roles (e.g., admin = 3, manager = 2, viewer = 1), the system can perform simple "greater-than-or-equal" comparisons rather than complex set intersections or bitwise operations. This approach ensures that higher-privileged roles automatically inherit permissions from lower ranks.

What happens if a user has the correct role but hasn't completed MFA verification?

The requireRole guard executes await mfaEmDivida() before evaluating role ranks. If multi-factor authentication is required but not satisfied, the function immediately returns a 403 MFA-Required error and logs an authz.denied audit event. This MFA-first policy ensures that role information is never exposed to sessions lacking proper authentication factors.

How does the platform ensure that API authorization matches database-level security?

Both the application-layer guard and Supabase Row Level Security (RLS) policies rely on the identical fn_user_role_in_org(p_org) RPC function. Defined as a SECURITY DEFINER PostgreSQL routine, this function serves as the single source of truth for role data. Because API routes and database policies query the same authoritative source, DeskcommCRM eliminates authorization inconsistencies between the Next.js application and direct database access.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →