How Plane Implements Its Permission System: A Deep Dive into the Codebase
Plane implements a type-safe, reactive permission system using enumerated roles, a centralized BaseUserPermissionStore, and a declarative USER_ALLOWED_PERMISSIONS map that drives access control across both workspace and project scopes.
The open-source project management tool Plane (makeplane/plane) handles complex authorization requirements through a layered architecture that separates role definitions from permission logic. Understanding how the Plane permission system works is essential for developers extending the platform or integrating custom access controls.
Core Role Enumerations and Workspace Hierarchy
Plane organizes users into three distinct permission layers, each represented by TypeScript enums that enforce type safety throughout the application.
Global and Scoped Role Definitions
The foundation rests on three enums defined in the @plane/constants and @plane/types packages. In packages/constants/src/user.ts, the EUserPermissions enum defines global workspace-level capabilities:
export enum EUserPermissions {
GUEST = 5,
MEMBER = 15,
ADMIN = 20
}
For more granular control, packages/types/src/enums.ts provides scoped variants. EUserWorkspaceRoles handles workspace-specific assignments, while EUserProjectRoles governs individual project access. These numeric values enable simple comparison operations—higher values indicate greater privileges.
Role Utility Helpers
The utility layer in packages/utils/src/permission/role.ts provides two critical functions for role manipulation. The getHighestRole function resolves the most privileged role from an array of assignments:
export const getHighestRole = <T extends TSupportedRole>(roles: T[]): T | undefined => {
if (!roles?.length) return undefined;
return roles.reduce((high, cur) => (cur > high ? cur : high));
};
This utility supports Plane's inheritance model where a user might hold different roles across multiple projects, requiring the system to determine effective permissions.
The Central Permission Store Architecture
At the heart of the Plane permission system lies the BaseUserPermissionStore located in apps/web/core/store/user/base-permissions.store.ts. This MobX-powered store caches permission state and exposes the primary authorization API used throughout the frontend.
BaseUserPermissionStore Implementation
The store maintains reactive state for workspaceProjectsPermissions, caching both workspace-wide and project-specific role assignments. It exposes two key lookup methods:
getWorkspaceRoleByWorkspaceSlug(workspaceSlug)– Retrieves the user's workspace-level permissiongetProjectRole(workspaceSlug, projectId?)– Resolves the highest project-specific role, falling back to workspace permissions when necessary
The allowPermissions method serves as the universal gatekeeper:
allowPermissions = (
allowPermissions: ETempUserRole[],
level: TUserPermissionsLevel,
workspaceSlug: string,
projectId?: string,
onPermissionAllowed?: () => boolean
): boolean => {
const role = level === EUserPermissionsLevel.WORKSPACE
? this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug)
: this.getProjectRole(workspaceSlug, projectId);
const hasPermission = allowPermissions.includes(role as any);
return hasPermission && (!onPermissionAllowed || onPermissionAllowed());
};
This method accepts a requested permission list, a level indicator (WORKSPACE or PROJECT), and an optional callback for custom validation logic.
Reactive Integration with UserStore
A concrete UserPermissionStore extends the base class and integrates into the main user store at apps/web/core/store/user/index.ts. This architecture ensures that UI components automatically recompute access rights when underlying role data changes—for example, when a user receives a promotion in real-time.
Declarative Permission Maps
Plane centralizes what each role may perform through the USER_ALLOWED_PERMISSIONS constant in packages/constants/src/user.ts. This declarative map separates permission definitions from business logic, enabling changes to access rules without modifying checking code.
USER_ALLOWED_PERMISSIONS Structure
The map uses a resource-action pattern where each feature (like labels or issues) specifies which roles may perform CRUD operations:
export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = {
PROJECT_LABELS: {
read: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
create: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
update: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
delete: [EUserPermissions.ADMIN],
},
// Additional resources: PROJECT_ISSUES, PROJECT_CYCLES, etc.
};
Because this configuration is data-driven, adding new resources requires only extending the constant object. The permission store references these arrays when evaluating access requests, comparing the user's resolved role against the allowed list.
Permission Checking in Practice
Frontend components interact with the permission system through the store's reactive methods, enabling both conditional rendering and API call protection.
UI Integration and Conditional Rendering
Components check permissions before rendering sensitive UI elements. For example, to conditionally show an "Add Label" button:
const canCreateLabel = permissionStore.allowPermissions(
USER_ALLOWED_PERMISSIONS.PROJECT_LABELS.create,
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
The system evaluates the current user's project role against the creation whitelist. If the user holds either ADMIN or MEMBER status, the function returns true.
Complete Implementation Examples
Checking issue creation rights:
import { EUserPermissionsLevel, USER_ALLOWED_PERMISSIONS } from "@plane/constants";
import { UserPermissionStore } from "./store/user";
function canCreateIssue(
permissionStore: UserPermissionStore,
workspaceSlug: string,
projectId: string
): boolean {
return permissionStore.allowPermissions(
USER_ALLOWED_PERMISSIONS.PROJECT_ISSUES.create,
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
}
Resolving the highest role across team members:
import { getHighestRole, EUserPermissions } from "@plane/utils/permission";
const roles = members.map(m => m.role);
const highest = getHighestRole(roles); // Returns EUserPermissions.ADMIN if any admin exists
Summary
- Plane's permission system uses numeric enums (
EUserPermissions,EUserWorkspaceRoles,EUserProjectRoles) where higher values indicate greater access levels. - The
BaseUserPermissionStoreinapps/web/core/store/user/base-permissions.store.tsprovides centralized, reactive permission state using MobX. - Role resolution occurs through
getWorkspaceRoleByWorkspaceSlugandgetProjectRole, with automatic fallback from project to workspace level. - The
allowPermissionsmethod validates access againstUSER_ALLOWED_PERMISSIONS, a declarative map separating configuration from logic. - The utility helpers
getUserRoleandgetHighestRoleinpackages/utils/src/permission/role.tshandle role comparison and array reduction. - All permission checks are type-safe and reactive, ensuring UI components update automatically when user roles change.
Frequently Asked Questions
How does Plane handle users with multiple roles across different projects?
Plane resolves effective permissions using the getHighestRole utility function from packages/utils/src/permission/role.ts. When a user belongs to multiple projects, the BaseUserPermissionStore evaluates each project assignment independently. For any specific action, the store compares the user's resolved role against the USER_ALLOWED_PERMISSIONS whitelist for that resource, using the highest privilege available in the current context.
What is the difference between EUserPermissions and EUserProjectRoles?
EUserPermissions defined in packages/constants/src/user.ts represents global workspace-level capabilities with values GUEST (5), MEMBER (15), and ADMIN (20). EUserProjectRoles in packages/types/src/enums.ts provides project-specific role granularity. While they often share similar names, the project roles allow for fine-grained access control within individual projects, falling back to workspace permissions when no specific project assignment exists.
Where does Plane store the active permission state for the current user?
The active permission state lives in the BaseUserPermissionStore located at apps/web/core/store/user/base-permissions.store.ts. This MobX store caches workspaceProjectsPermissions and provides reactive getters like getWorkspaceRoleByWorkspaceSlug. The store is instantiated within the main user store at apps/web/core/store/user/index.ts, making permission data available throughout the React component tree via the application's state management system.
Can I modify Plane's permission rules without changing the core logic?
Yes. Plane uses a declarative permission map called USER_ALLOWED_PERMISSIONS in packages/constants/src/user.ts. This constant object defines which roles can perform specific actions on resources like issues, labels, and cycles. By editing this configuration object—adding or removing roles from the read, create, update, or delete arrays—you can modify access control rules without touching the allowPermissions validation logic or any UI components.
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 →