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:
- Database Layer: Update
ROLE_CHOICESinapps/api/plane/db/models/workspace.pyorproject.pywith new integer values - Backend Logic: Modify relevant permission classes in
apps/api/plane/utils/permissions/workspace.pyto reference the new role constants - Frontend Sync: Add corresponding constants to
apps/web/ce/store/user/permission.store.ts - 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
WorkspaceMemberandProjectMembermodels. - DRF permission classes in
apps/api/plane/utils/permissions/workspace.pyenforce access rules through database validation of role andis_activestatus. - The frontend permission store mirrors backend constants in
apps/web/ce/store/user/permission.store.tsfor immediate UI state management. - The
allow_permissionhelper inbase.pyprovides 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →