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

> Understand how Plane separates workspace and project settings and permissions. Learn about its hierarchical access control enforcing strict control over your Plane projects.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: architecture
- Published: 2026-06-23

---

**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`](https://github.com/makeplane/plane/blob/main/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**.

```python

# 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`](https://github.com/makeplane/plane/blob/main/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.

```python

# 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/workspace.py))

The `WorkSpaceBasePermission` class implements the core workspace authorization logic:

```python

# 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/project.py))

Project permissions follow an analogous pattern in [`apps/api/plane/app/permissions/project.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/project.py), using `ProjectMember` queries instead:

```python

# 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/workspace.py). The `WorkspaceUserPreferenceViewSet` in the views layer ensures users can only modify their own preference records.

```python

# 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/project.py) expose these fields while `ProjectEntityPermission` guards access.

```python

# 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:

```bash
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`](https://github.com/makeplane/plane/blob/main/workspace.py) vs [`project.py`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/workspace.py) and [`apps/api/plane/app/permissions/project.py`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.