Plane Permission and Role System Architecture: A Deep Dive into RBAC Implementation

Plane implements a role-based access control (RBAC) system using integer-based roles (Admin=20, Member=15, Guest=5) stored in membership models, enforced through Django REST Framework permission classes and mirrored in a TypeScript front-end store.

The open-source project management platform Plane (makeplane/plane) separates permission logic across its Django backend and Next.js frontend using a shared integer enum system. This architecture enables consistent access control for both workspace-level and project-level resources while maintaining a single source of truth in the database. Understanding this Plane permission and role system architecture is essential for customizing access controls or contributing to the codebase.

Core Role Constants and Storage

Plane's RBAC system relies on three primary role levels represented as integer constants. These values are synchronized between the backend database and frontend application.

Backend Role Definitions

In the Django models, roles are stored as PositiveSmallIntegerField with predefined choices. The WorkspaceMember model in apps/api/plane/db/models/workspace.py and the ProjectMember model in apps/api/plane/db/models/project.py both implement identical role storage:

  • Admin = 20 (full administrative access)
  • Member = 15 (standard collaborative access)
  • Guest = 5 (read-only or limited access)

Both membership models include an is_active boolean flag that permission classes validate alongside role checks.

Frontend Mirror Store

The TypeScript frontend maintains identical constants in apps/web/ce/store/user/permission.store.ts:

export const Admin = 20;
export const Member = 15;
export const Guest = 5;

This mirroring allows the React frontend to make immediate UI decisions without waiting for backend round-trips, while ensuring consistency with the database schema.

Backend Permission Architecture

The backend implements access control through Django REST Framework (DRF) BasePermission subclasses located in apps/api/plane/utils/permissions/workspace.py.

Primary Permission Classes

Plane defines granular permission classes for different access patterns:

  • WorkSpaceBasePermission: Handles general workspace actions with method-specific role requirements. Grants POST to any authenticated user, SAFE_METHODS to active members, PUT/PATCH to Admin or Member roles, and DELETE exclusively to Admin.
  • WorkspaceOwnerPermission: Restricts access strictly to Admin role holders (role = 20).
  • WorkSpaceAdminPermission: Allows both Admin and Member roles (role ∈ [20, 15]) for edit operations.
  • WorkspaceEntityPermission: Controls access to workspace entities like issues and cycles, requiring Admin or Member status for unsafe methods.
  • WorkspaceViewerPermission: Grants read-only access to any active member regardless of role.

Reusable Permission Helper

The allow_permission function in apps/api/plane/utils/permissions/base.py provides a generic mechanism for role validation:

def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
    # Convert enum members to integer values

    allowed_role_values = [
        role.value if isinstance(role, ROLE) else role for role in allowed_roles
    ]
    # Performs filtered query against membership tables

    return MembershipModel.objects.filter(
        member=request.user,
        role__in=allowed_role_values,
        is_active=True,
    ).exists()

This helper supports both workspace-level and project-level checks through the level parameter.

Frontend Permission Store

The client-side permission system centers on a lightweight store that caches the current user's role from API endpoints like /api/workspaces/:slug/members/.

Implementation Pattern

Components import role constants and validate against the cached role value:

import { usePermission } from '@/store/user';
import { Admin, Member } from '@/store/user/permission.store';

function EditWorkspaceButton() {
  const { role } = usePermission(); // pulls role from the store

  if (![Admin, Member].includes(role)) {
    return null; // hide button for guests
  }

  return <button>Edit Workspace</button>;
}

This pattern ensures UI elements remain consistent with backend enforcement while providing immediate feedback to users.

Permission Checking Flow

When a request reaches a DRF view, Plane evaluates permissions through the has_permission method chain. The system performs an ORM query against either WorkspaceMember or ProjectMember tables to verify both role eligibility and active status.

API View Implementation

Developers apply permissions declaratively in view classes:

from plane.utils.permissions.workspace import WorkspaceOwnerPermission
from rest_framework.views import APIView

class WorkspaceDeleteView(APIView):
    permission_classes = [WorkspaceOwnerPermission]

    def delete(self, request, *args, **kwargs):
        # Only workspace owners (role = Admin) reach this point

        # Deletion logic here

        pass

The permission class automatically handles the database lookup, checking that request.user has an active WorkspaceMember record with role=20 before allowing the DELETE operation.

Extending the System

Adding new roles or permission levels requires coordinated changes across the stack:

  1. Database Layer: Update ROLE_CHOICES in apps/api/plane/db/models/workspace.py or project.py with new integer values
  2. Backend Logic: Modify relevant permission classes in apps/api/plane/utils/permissions/workspace.py to reference the new role constants
  3. Frontend Sync: Add corresponding constants to apps/web/ce/store/user/permission.store.ts
  4. UI Updates: Implement conditional rendering logic using the new role values

Project-level permissions follow an identical pattern to workspace permissions, with implementations located in apps/api/plane/utils/permissions/project.py querying the ProjectMember model.

Summary

  • Plane uses integer-based RBAC with Admin (20), Member (15), and Guest (5) roles stored in WorkspaceMember and ProjectMember models.
  • DRF permission classes in apps/api/plane/utils/permissions/workspace.py enforce access rules through database validation of role and is_active status.
  • The frontend permission store mirrors backend constants in apps/web/ce/store/user/permission.store.ts for immediate UI state management.
  • The allow_permission helper in base.py provides reusable logic for custom permission implementations across both workspace and project contexts.

Frequently Asked Questions

How does Plane handle permission inheritance between workspace and project levels?

Plane maintains separate membership models for workspaces and projects. While a user might have Admin access at the workspace level, their project-level permissions are determined by separate ProjectMember records in apps/api/plane/db/models/project.py. The system does not automatically inherit workspace roles into projects; each membership is validated independently through project-specific permission classes in apps/api/plane/utils/permissions/project.py.

Can custom roles be added to Plane's permission system without modifying core code?

Yes, custom roles require extending the ROLE_CHOICES enum in the Django models and adding corresponding integer constants to the TypeScript store. You must also create new permission classes or extend existing ones in apps/api/plane/utils/permissions/workspace.py to recognize the new role values. The modular architecture allows these extensions without altering the base permission logic.

What happens when a workspace member's is_active flag is set to false?

Permission classes check both role eligibility and the is_active boolean flag. When is_active=False, the user effectively loses all access regardless of their role value. The allow_permission helper and individual permission classes like WorkspaceViewerPermission explicitly filter for is_active=True, ensuring deactivated members cannot read or modify workspace resources.

Where does the frontend retrieve the current user's role information?

The permission store fetches role data from the API endpoint /api/workspaces/:slug/members/ (or project-specific equivalents) and caches it locally. This cached value persists in the TypeScript store located at apps/web/ce/store/user/permission.store.ts, allowing components to reference usePermission() hooks for immediate access control decisions without repeated network requests.

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 →