# Plane Django API Backend Architecture: A Deep Dive into Service Layers and Code Organization

> Explore the modular Django API backend architecture of Plane and its distinct service layers. Understand routing, business logic, and data access within the makeplane repository for efficient development.

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

---

**Plane's Django API backend follows a modular, layered architecture that separates routing, business logic, data access, and background processing into distinct Django apps within the `makeplane/plane` repository.**

This architecture leverages Django REST Framework (DRF) for API endpoints, Celery for asynchronous tasks, and a comprehensive middleware stack for cross-cutting concerns. The codebase organizes functionality into logical domains—such as `plane.api`, `plane.db`, and `plane.bgtasks`—promoting clear separation of concerns and maintainable service layers.

## Routing Layer and URL Resolution

The entry point for all HTTP requests is the root URL dispatcher located in [`plane/urls.py`](https://github.com/makeplane/plane/blob/main/plane/urls.py). This module mounts sub-routers for different API surfaces and administrative interfaces.

```python

# plane/urls.py

urlpatterns = [
    path("api/", include("plane.app.urls")),
    path("api/public/", include("plane.space.urls")),
    path("api/instances/", include("plane.license.urls")),
    path("api/v1/", include("plane.api.urls")),
    path("auth/", include("plane.authentication.urls")),
    path("", include("plane.web.urls")),
]

```

*Source*: [[`plane/urls.py`](https://github.com/makeplane/plane/blob/main/plane/urls.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/urls.py)

This design isolates public APIs, authenticated internal endpoints, and web frontend routes into separate URL configurations, enabling independent versioning and access control policies for each surface area.

## Core Application Structure

The Django project organizes functionality into discrete apps registered in `INSTALLED_APPS` within [`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py). This modular approach separates concerns across analytics, authentication, database models, and utility functions.

```python

# plane/settings/common.py

INSTALLED_APPS = [
    # Django core

    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.staticfiles",
    # In-house apps

    "plane.analytics",
    "plane.app",
    "plane.space",
    "plane.bgtasks",
    "plane.db",
    "plane.utils",
    "plane.web",
    "plane.middleware",
    "plane.license",
    "plane.api",
    "plane.authentication",
    # Third-party

    "rest_framework",
    "corsheaders",
    "django_celery_beat",
]

```

*Source*: [[`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/settings/common.py#L96-L114)

The `plane.db` app houses core domain models for issues, cycles, and projects, while `plane.app` and `plane.api` contain the primary business logic and REST endpoints respectively.

## Views, Serializers, and Data Flow

The API layer implements the **Model-View-Serializer** pattern using Django REST Framework. Viewsets in `plane/api/views/` handle HTTP semantics, while serializers in `plane/api/serializers/` manage validation and JSON conversion.

**View Implementation:**

```python

# plane/api/views/issue.py

class IssueViewSet(viewsets.ModelViewSet):
    queryset = Issue.objects.all()
    serializer_class = IssueSerializer
    permission_classes = [IsAuthenticated, IssuePermission]

```

*Source*: [[`issue.py`](https://github.com/makeplane/plane/blob/main/issue.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/api/views/issue.py)

**Corresponding Serializer:**

```python

# plane/api/serializers/issue.py

class IssueSerializer(serializers.ModelSerializer):
    class Meta:
        model = Issue
        fields = "__all__"

```

*Source*: [[`issue.py`](https://github.com/makeplane/plane/blob/main/issue.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/api/serializers/issue.py)

This separation ensures that data validation rules remain decoupled from HTTP transport concerns, allowing serializers to be reused across different view contexts.

## Access Control and Query Filtering

Fine-grained authorization logic resides in `plane/utils/permissions/`, implementing DRF's `BasePermission` interface for object-level access control.

```python

# plane/utils/permissions/workspace.py

class WorkspacePermission(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.workspace.is_member(request.user)

```

*Source*: [[`workspace.py`](https://github.com/makeplane/plane/blob/main/workspace.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/utils/permissions/workspace.py)

Dynamic query filtering utilizes **django-filter** backends defined in `plane/utils/filters/`:

```python

# plane/utils/filters/filterset.py

class IssueFilter(FilterSet):
    status = CharFilter(field_name="status")
    assigned_to = NumberFilter(field_name="assignee__id")

```

*Source*: [[`filterset.py`](https://github.com/makeplane/plane/blob/main/filterset.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/utils/filters/filterset.py)

These filters integrate directly with viewsets to enable complex query parameters without cluttering view logic.

## Middleware Stack and Request Processing

The middleware pipeline in [`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py) processes requests through a chain of security, logging, and routing layers.

```python

# plane/settings/common.py

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "plane.authentication.middleware.session.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "plane.middleware.request_body_size.RequestBodySizeLimitMiddleware",
    "plane.middleware.logger.APITokenLogMiddleware",
    "plane.middleware.logger.RequestLoggerMiddleware",
]

```

*Source*: [[`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/settings/common.py#L20-L36)

Custom middleware components handle cross-cutting concerns such as **request body size limiting**, **API token logging**, and **database routing** for read-replicas, keeping these concerns out of business logic.

## Background Task Processing with Celery

Asynchronous operations—such as email notifications, webhook deliveries, and data exports—are offloaded to **Celery** workers defined in `plane/bgtasks/`.

```python

# plane/bgtasks/email_notification_task.py

@app.task
def email_notification_task(user_id, subject, body):
    user = User.objects.get(id=user_id)
    send_mail(subject, body, from_email, [user.email])

```

*Source*: [[`email_notification_task.py`](https://github.com/makeplane/plane/blob/main/email_notification_task.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/bgtasks/email_notification_task.py)

Celery configuration specifies the broker URL and task imports:

```python

# plane/settings/common.py

CELERY_BROKER_URL = os.getenv("AMQP_URL", f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}")
CELERY_IMPORTS = (
    "plane.bgtasks.issue_automation_task",
    "plane.bgtasks.exporter_expired_task",
)

```

*Source*: [[`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py)](https://github.com/makeplane/plane/blob/preview/apps/api/plane/settings/common.py#L26-L38)

This architecture prevents long-running operations from blocking HTTP request/response cycles.

## External Services and Configuration

The backend integrates with external infrastructure through dedicated settings modules:

- **Storage**: [`plane/settings/storage.py`](https://github.com/makeplane/plane/blob/main/plane/settings/storage.py) configures S3/MinIO backends for static and media files
- **Caching**: [`plane/settings/redis.py`](https://github.com/makeplane/plane/blob/main/plane/settings/redis.py) defines Redis connections for caching and Celery result storage
- **Documentation**: [`plane/settings/openapi.py`](https://github.com/makeplane/plane/blob/main/plane/settings/openapi.py) configures DRF Spectacular for OpenAPI schema generation when `ENABLE_DRF_SPECTACULAR` is enabled

These configurations are centralized in [`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py), ensuring environment-specific variables remain separated from application code.

## Summary

- **Modular app structure**: The backend splits functionality across `plane.api`, `plane.app`, `plane.db`, and `plane.bgtasks` for clear separation of concerns
- **DRF-based API layer**: Viewsets and serializers in `plane/api/` implement REST endpoints with standardized validation and permission checks
- **Comprehensive middleware**: Custom middleware in `plane/middleware/` handles logging, body-size limits, and database routing
- **Async task processing**: Celery workers in `plane/bgtasks/` handle notifications and background jobs via RabbitMQ/Redis
- **Centralized configuration**: Settings modules organize database, cache, storage, and authentication parameters in `plane/settings/`

## Frequently Asked Questions

### How does Plane handle database routing and read replicas?

The middleware stack includes a custom database routing layer that directs read queries to replica databases while maintaining write operations on the primary instance. This configuration lives within the `MIDDLEWARE` list in [`plane/settings/common.py`](https://github.com/makeplane/plane/blob/main/plane/settings/common.py) and works alongside Django's database router configuration to distribute query load.

### What permission system does Plane use for workspace and project access?

Plane implements **object-level permissions** through DRF's `BasePermission` classes located in `plane/utils/permissions/`. The `WorkspacePermission` and `ProjectPermission` classes check membership relationships via methods like `is_member()` on workspace and project objects, ensuring users can only access resources within their authorized organizations.

### How are background tasks triggered from API views?

Views enqueue Celery tasks using the `.delay()` method on task functions imported from `plane.bgtasks/`. For example, when an issue is created, the view calls `email_notification_task.delay()` to asynchronously send notifications without blocking the HTTP response, as implemented in the viewsets within `plane/api/views/`.

### Where does Plane store uploaded files and static assets?

File storage configuration resides in [`plane/settings/storage.py`](https://github.com/makeplane/plane/blob/main/plane/settings/storage.py), which supports S3-compatible storage via MinIO. The system uses Django's storages backend abstraction to handle media uploads and static file collection, allowing deployment flexibility across different cloud providers or on-premises infrastructure.