arrow_backTech Blog
dns
dnsBackend2026-02-18·10 min read

Flexible RBAC Permission System with NestJS + Prisma

RBAC implementation in a SaaS system — data model design, Guard interception, decorator patterns, and API permission validation flow.

NestJSPrismaRBAC

Permission System Design

In a SaaS membership system, different roles (super admin, operator, regular user) need different feature permissions.

Permission systems are B2B product infrastructure — nobody notices when it's right, everyone finds you when it's wrong.

Our requirements:

  • Different departments within the same company need different permissions
  • Admins can customize role and permission combinations
  • Fine-grained API-level control (not just page-level)
  • Permission changes take effect immediately without re-login
  • Data Model

    Using Prisma to define User, Role, Permission tables with many-to-many relations for flexible permission assignment.

    Core Table Design

  • UserBasic info + organization membership
  • RoleRole name + description + organization scope
  • PermissionPermission identifier (e.g., `user:create`, `order:read`) + resource + action
  • UserRoleUser-Role many-to-many junction
  • RolePermissionRole-Permission many-to-many junction
  • 💡

    Use `resource:action` format for permission identifiers (e.g., `user:create`) — semantically clear and supports wildcard matching.

    Permission Inheritance

    Support role hierarchy — super admins automatically have all permissions, department managers have all department permissions. Implemented via parent_role_id for recursive inheritance.


    NestJS Guard Implementation

    Custom `@Roles()` decorator and `RolesGuard` for unified API access control at the Controller layer.

    Decorator Design

    We defined two decorators:

  • @Roles('admin', 'operator')Coarse-grained role-based control
  • @RequirePermissions('user:create', 'user:update')Fine-grained permission-based control
  • Guard Execution Flow

    1
    Extract TokenGet JWT from request headers
    2
    Parse User InfoVerify token, get userId
    3
    Query PermissionsFetch user permission list from Redis cache
    4
    Permission CheckCompare decorator-declared permissions with actual user permissions
    5
    Allow or DenyReturn 403 Forbidden if insufficient permissions
    Guards are the last line of defense, but shouldn't be the only one. Frontend should also hide unauthorized buttons and menus for better UX.

    Caching Strategy

    Redis Cache

    Cache user permissions in Redis to avoid database queries per request. Key design:

  • `permissions:{userId}` → Complete user permission list
  • TTL set to 5 minutes, proactively cleared on permission changes
  • Cache Invalidation

    When admins modify role permissions:

    1

    Update role-permission associations in database

    2

    Find all users with that role

    3

    Batch-delete their Redis caches

    4

    Cache automatically rebuilds on next request

    ⚠️

    Watch performance during batch cache clearing — if a role has thousands of users, sequential deletion is slow. Use Redis pipeline for batch operations.

    Frontend-Backend Sync

    Backend returns permission list, frontend dynamically renders menus and buttons.

    Frontend Permission Control

  • Route levelRoute guards check page access permissions
  • Component level`<PermissionGate permission="user:create">` wrapper component
  • Button levelUnauthorized buttons shown as disabled or hidden entirely
  • Login Flow

    On successful login, return all permission identifiers at once. Frontend stores them in state management. Permission changes pushed via WebSocket.


    Testing Strategy

    Permission system testing is critical — one bug could mean data leakage.

  • Unit testsVerify each Guard's permission logic
  • Integration testsSimulate different-role users accessing various API endpoints
  • E2E testsComplete role creation → permission assignment → API access flow
  • Our principle: Permission-related code must have 100% test coverage. No exceptions.

    Conclusion

    RBAC seems simple but achieving flexibility, efficiency, and security isn't easy. Keys: extensible data model, smart caching, frontend-backend coordination. This system has been running stable for 1+ year in our SaaS, supporting 200+ customer organizations with different permission configs.

    💡

    For simple needs (a few fixed roles), you don't need this complexity. Just hardcode role checks in Guards. An over-engineered permission system is more dangerous than none.

    dns