How Plane Handles Workspace-Level vs Project-Level Settings and Permissions

Plane separates workspace and project concerns through distinct Django models, permission classes, and membership roles, enforcing strict hierarchical access control where workspace membership is required for project creation but project permissions are scoped independently.

Plane, the open-source project management platform from makeplane/plane, implements a two-tier permission system that distinguishes between workspace-level settings (global configurations affecting all projects) and project-level settings (specific to individual initiatives). This architecture ensures that navigation preferences, themes, and workspace-wide roles are managed separately from issue-type defaults and deploy-board configurations.

Data Model Architecture

Plane’s data layer explicitly separates workspace and project entities using soft-delete patterns (deleted_at nullable) and unique constraints on preference keys.

Workspace Model (apps/api/plane/db/models/workspace.py)

The Workspace model governs global settings through fields like navigation_control_preference, theme, and generic JSON blobs for extensible configuration. The WorkspaceMember model links Django users to workspaces via a numeric role column where Admin = 20, Member = 15, and Guest = 5.


# Conceptual model structure from apps/api/plane/db/models/workspace.py

class Workspace(BaseModel):
    name = models.CharField(max_length=80)
    slug = models.SlugField(unique=True)
    # Global settings

    navigation_control_preference = models.JSONField(default=dict)
    theme = models.JSONField(default=dict)
    deleted_at = models.DateTimeField(null=True)

Project Model (apps/api/plane/db/models/project.py)

The Project model maintains its own preferences JSONField (defaulted by get_default_preferences) alongside project-specific columns like public_at and archived_at. The ProjectMember model mirrors WorkspaceMember with identical role semantics but scopes membership to individual projects.


# From apps/api/plane/db/models/project.py

class Project(BaseModel):
    name = models.CharField(max_length=255)
    workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE)
    preferences = models.JSONField(default=get_default_preferences)
    public_at = models.DateTimeField(null=True)
    archived_at = models.DateTimeField(null=True)

Membership and Role Hierarchy

Both membership models use the same integer-based role system:

  • Admin (20): Full delete permissions and settings modification
  • Member (15): Read and write access (PUT/PATCH)
  • Guest (5): Read-only access

Workspace membership is required to create projects, but project roles are evaluated independently through separate permission classes.

Permission Class Implementation

All permission classes inherit from rest_framework.permissions.BasePermission and perform request-scoped checks against workspace_slug or project_slug parameters.

Workspace-Level Permissions (apps/api/plane/app/permissions/workspace.py)

The WorkSpaceBasePermission class implements the core workspace authorization logic:


# apps/api/plane/app/permissions/workspace.py

from rest_framework.permissions import BasePermission, SAFE_METHODS

class WorkSpaceBasePermission(BasePermission):
    def has_permission(self, request, view):
        # POST → anyone authenticated may create a workspace

        if request.method == "POST":
            return True

        # SAFE_METHODS (GET, HEAD, OPTIONS) → any active member may read

        if request.method in SAFE_METHODS:
            return True

        # PUT / PATCH → only Admins or Members may modify workspace settings

        if request.method in ["PUT", "PATCH"]:
            return WorkspaceMember.objects.filter(
                member=request.user,
                workspace__slug=view.workspace_slug,
                role__in=[Admin, Member],
                is_active=True,
            ).exists()

        # DELETE → only Admins (owners) may delete a workspace

        if request.method == "DELETE":
            return WorkspaceMember.objects.filter(
                member=request.user,
                workspace__slug=view.workspace_slug,
                role=Admin,
                is_active=True,
            ).exists()

Additional specialized classes like WorkspaceOwnerPermission, WorkspaceViewerPermission, and WorkspaceEntityPermission provide thin wrappers that check specific role thresholds against the requesting user.

Project-Level Permissions (apps/api/plane/app/permissions/project.py)

Project permissions follow an analogous pattern in apps/api/plane/app/permissions/project.py, using ProjectMember queries instead:


# apps/api/plane/app/permissions/project.py

class ProjectBasePermission(BasePermission):
    def has_permission(self, request, view):
        if request.user.is_anonymous:
            return False

        # POST → any authenticated user may create a project inside a workspace they belong to

        if request.method == "POST":
            return WorkspaceMember.objects.filter(
                member=request.user,
                workspace__slug=view.workspace_slug,
                is_active=True,
            ).exists()

        # SAFE_METHODS → any active project member can read

        if request.method in SAFE_METHODS:
            return ProjectMember.objects.filter(
                member=request.user,
                project__slug=view.project_slug,
                is_active=True,
            ).exists()

        # PUT / PATCH → only Admins or Members can modify project data

        if request.method in ["PUT", "PATCH"]:
            return ProjectMember.objects.filter(
                member=request.user,
                project__slug=view.project_slug,
                role__in=[Admin, Member],
                is_active=True,
            ).exists()

        # DELETE → only Admins (owners) can delete a project

        if request.method == "DELETE":
            return ProjectMember.objects.filter(
                member=request.user,
                project__slug=view.project_slug,
                role=Admin,
                is_active=True,
            ).exists()

The ProjectEntityPermission and ProjectLitePermission classes extend this logic for specific view requirements.

Settings Storage and Endpoints

Plane exposes settings through RESTful endpoints that enforce the permission classes described above.

Workspace Settings and Preferences

Workspace-level preferences (e.g., sidebar pinned items, home layout) are stored in the WorkspaceUserPreference model within apps/api/plane/db/models/workspace.py. The WorkspaceUserPreferenceViewSet in the views layer ensures users can only modify their own preference records.


# apps/api/plane/app/views/workspace/base.py

from plane.app.permissions.workspace import WorkspaceEntityPermission

class WorkspaceSettingsEndpoint(BaseAPIView):
    permission_classes = [WorkspaceEntityPermission]

    def get(self, request, slug):
        workspace = Workspace.objects.get(slug=slug)
        serializer = WorkspaceSerializer(workspace)
        return Response(serializer.data)

    def patch(self, request, slug):
        workspace = Workspace.objects.get(slug=slug)
        serializer = WorkspaceSerializer(workspace, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)

Endpoints follow the pattern:

  • GET /api/workspaces/{slug}/settings/ (read)
  • PATCH /api/workspaces/{slug}/settings/ (update)

Project Settings Configuration

Project settings reside in the preferences JSONField on the Project model. Serializers in apps/api/plane/app/serializers/project.py expose these fields while ProjectEntityPermission guards access.


# apps/api/plane/app/views/project/base.py

from plane.app.permissions.project import ProjectEntityPermission

class ProjectSettingsEndpoint(BaseAPIView):
    permission_classes = [ProjectEntityPermission]

    def get(self, request, project_slug):
        project = Project.objects.get(slug=project_slug)
        return Response(ProjectSerializer(project).data)

    def patch(self, request, project_slug):
        project = Project.objects.get(slug=project_slug)
        ser = ProjectSerializer(project, data=request.data, partial=True)
        ser.is_valid(raise_exception=True)
        ser.save()
        return Response(ser.data)

Preference Isolation

The system maintains strict isolation between layers:

  • Workspace preferences: Row-level security in WorkspaceUserPreference (user-specific)
  • Project preferences: JSONField on the Project model (shared among project members with appropriate roles)

Request Flow and Authorization Logic

When a client interacts with Plane’s API, the authorization flow proceeds as follows:

  1. URL Resolution: Django extracts workspace_slug or project_slug from the request path
  2. Permission Check: The view’s permission_classes (e.g., WorkspaceEntityPermission or ProjectBasePermission) query the corresponding membership table
  3. Role Evaluation: The permission class compares the user’s role integer against the required threshold for the HTTP method
  4. Data Access: If permitted, the view reads from or writes to Workspace, Project, or preference models

For example, updating a workspace preference via cURL:

curl -X PATCH \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"is_pinned": true, "sort_order": 10}' \
  https://plane.example.com/api/workspaces/my-workspace/settings/preferences/sidebar/

Only the requesting user’s WorkspaceUserPreference row is modified, enforced by the viewset’s query filtering.

Summary

  • Plane separates workspace and project settings through distinct models (Workspace vs Project) and permission modules (workspace.py vs project.py).
  • Role-based access control uses numeric levels (Admin=20, Member=15, Guest=5) implemented in WorkspaceMember and ProjectMember models.
  • Workspace permissions in apps/api/plane/app/permissions/workspace.py govern global settings and require active membership for read access, with Admin/Member roles for modifications.
  • Project permissions in apps/api/plane/app/permissions/project.py scope access to individual projects, checking ProjectMember records while requiring underlying workspace membership for creation.
  • Preferences are isolated: Workspace UI preferences use the WorkspaceUserPreference model for per-user storage, while project settings use JSONFields on the Project model shared among authorized members.

Frequently Asked Questions

What is the difference between workspace and project permissions in Plane?

Workspace permissions control access to global resources like workspace settings, themes, and navigation preferences defined in apps/api/plane/db/models/workspace.py, while project permissions govern issue tracking, deploy boards, and project-specific configurations in apps/api/plane/db/models/project.py. Workspace permission classes check WorkspaceMember roles, whereas project permission classes evaluate ProjectMember records independently.

How does Plane store user preferences at the workspace level versus project level?

Workspace-level preferences (such as sidebar configuration) are stored in the WorkspaceUserPreference model defined in apps/api/plane/db/models/workspace.py, ensuring each user has isolated preference rows accessible only to that user. Project-level preferences are stored as JSON in the preferences field of the Project model in apps/api/plane/db/models/project.py, making them shared configuration available to all authorized project members with appropriate roles.

What role levels exist in Plane and what permissions do they grant?

Plane uses three numeric role levels defined in the membership models: Admin (20) grants deletion rights and full settings modification capability, Member (15) allows creation and modification of resources via PUT/PATCH requests, and Guest (5) provides read-only access. These integer values are checked against request methods by permission classes in apps/api/plane/app/permissions/workspace.py and apps/api/plane/app/permissions/project.py.

Can a user create a project without being a workspace member?

No. According to the ProjectBasePermission implementation in apps/api/plane/app/permissions/project.py, the POST method explicitly requires an active WorkspaceMember record before allowing project creation. However, accessing or modifying existing projects depends on ProjectMember permissions, which are evaluated independently of ongoing workspace membership verification for read and update operations.

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 →