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
On this page

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

  • User: Basic info + organization membership
  • Role: Role name + description + organization scope
  • Permission: Permission identifier (e.g., user:create, order:read) + resource + action
  • UserRole: User-Role many-to-many junction
  • RolePermission: Role-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 Token: Get JWT from request headers
  2. Parse User Info: Verify token, get userId
  3. Query Permissions: Fetch user permission list from Redis cache
  4. Permission Check: Compare decorator-declared permissions with actual user permissions
  5. Allow or Deny: Return 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 level: Route guards check page access permissions
  • Component level: <PermissionGate permission="user:create"> wrapper component
  • Button level: Unauthorized 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 tests: Verify each Guard's permission logic
  • Integration tests: Simulate different-role users accessing various API endpoints
  • E2E tests: Complete 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