Learn from OSS
Plane

Architecture Overview

Plane's three-tier architecture — database, API, frontend, state management, and real-time services

Architecture Overview

For Product Managers

This page explains how Plane is structured as a software system. You don't need to understand every technical detail — focus on the diagrams and the "Why It Matters" callouts to build a mental model of how the product works under the hood.

System Architecture Diagram

Plane follows a three-tier architecture with a real-time collaboration layer, served through a Caddy reverse proxy:

┌─────────────────────────────────────────────────────────────────┐
│                         USERS (Browser)                         │
└──────────────────────────────┬──────────────────────────────────┘

                    ┌──────────▼──────────┐
                    │  Caddy Proxy (2.10)  │  ← automatic HTTPS
                    │  Port 80 / 443       │    routes by path
                    └──┬──────┬───────┬───┘
                       │      │       │
       ┌───────────────▼─┐ ┌──▼─────┐ ┌▼───────────────┐
       │  Web / Admin /   │ │  API   │ │  Live Server    │
       │  Space (React)   │ │(Django │ │ (Hocuspocus)    │
       │  Nginx           │ │ DRF)   │ │ WebSocket CRDT  │
       │  :3000/3001/3002 │ │ :8000  │ │ :3100           │
       └──────────────────┘ └──┬─────┘ └─────────────────┘

              ┌────────────────┼────────────────┐
              │                │                │
       ┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼───────┐
       │ PostgreSQL   │ │   Valkey    │ │  MinIO / S3   │
       │   15.7       │ │   7.2.11    │ │ (File Storage)│
       │ (Primary DB) │ │ (Cache)     │ │               │
       └──────────────┘ └─────────────┘ └───────────────┘

                        ┌──────▼──────┐
                        │  RabbitMQ   │
                        │   3.13.6    │
                        │ (MQ Broker) │
                        └──────┬──────┘

                        ┌──────▼──────┐
                        │   Celery    │
                        │  Workers    │
                        │  + Beat     │
                        └─────────────┘

Why It Matters

Every user request flows through Caddy → the appropriate service. REST API calls hit Django; real-time editing uses WebSockets to the Hocuspocus live server. Background work (email notifications, cleanup, imports) runs through Celery workers using RabbitMQ as the message broker. Valkey (Redis-compatible) handles caching and sessions. This separation means the API stays fast even when heavy async work is running.

The Three Tiers

1. Frontend Layer (React 18 + MobX)

The frontend is built with React 18, React Router 7 (with SSR support), and MobX for state management. Three separate apps share packages:

AppPortPurpose
web3000Main user-facing application (issues, boards, pages)
admin3001Instance administration (/god-mode/)
space3002Public-facing project views (shareable links)

MobX Store Hierarchy (180+ files):

RootStore
  ├── router.store           → routing state
  ├── theme.store             → UI theme
  ├── instance.store          → instance config
  ├── user/
  │     ├── profile.store     → current user
  │     ├── account.store     → auth account
  │     └── settings.store    → user preferences
  ├── workspace/
  │     ├── index.ts          → workspace data
  │     ├── home.ts           → home dashboard
  │     ├── webhook.store     → webhook management
  │     └── api-token.store   → API tokens
  ├── project/
  │     ├── project.store     → project CRUD
  │     └── project_filter    → view filters
  ├── issue/                  → issue stores per context
  │     ├── project/          → project issue list
  │     ├── cycle/            → cycle issues
  │     ├── module/           → module issues
  │     └── workspace-draft/  → draft issues
  ├── pages/                  → wiki pages
  ├── notifications/          → notification center
  ├── timeline/               → activity timeline
  └── ...

Why MobX + SWR?

MobX provides fine-grained reactivity: when a single issue's title changes, only the components rendering that title re-render. SWR complements MobX by handling data fetching and cache invalidation. MobX stores hold the canonical state; SWR fetches and revalidates from the API.

2. API Layer (Django 4.2 + DRF)

The backend is Django 4.2.29 LTS with Django REST Framework 3.15.2, living at apps/api/.

URL Routing Structure:

/api/             → plane.app.urls       (main app — workspaces, projects, issues)
/api/public/      → plane.space.urls     (public/anonymous endpoints)
/api/instances/   → plane.license.urls   (instance management)
/api/v1/          → plane.api.urls       (API key–based access)
/auth/            → plane.authentication (login, OAuth, magic links)

App-level URL modules (20+):

plane/app/urls/
  ├── analytic.py, api.py, asset.py, cycle.py, estimate.py
  ├── external.py, intake.py, issue.py, module.py, notification.py
  ├── page.py, project.py, search.py, state.py, user.py
  ├── views.py, webhook.py, workspace.py, timezone.py, exporter.py

Key API Patterns:

PatternDescription
Hierarchical URLsWorkspaces → Projects → Issues/Cycles/Modules
Standard RESTGET, POST, PATCH, DELETE
Throttling30/min anon, 60/min API key, custom per-endpoint
Serializer validationDRF serializers validate all input/output
API documentationdrf-spectacular (OpenAPI/Swagger)

3. Database Layer (PostgreSQL 15.7)

PostgreSQL with 30+ model files and 126 migrations.

Core Entity-Relationship Map:

                         ┌──────────────┐
                         │   Workspace  │ ← multi-tenancy root
                         └──────┬───────┘
                                │ has many
                    ┌───────────┼───────────┐
                    │           │           │
             ┌──────▼──────┐   │    ┌──────▼──────┐
             │   Project    │   │    │   Member    │
             └──────┬───────┘   │    └─────────────┘
                    │           │
         ┌──────────┼──────┬───┼──────┐
         │          │      │   │      │
   ┌─────▼────┐ ┌──▼───┐ ┌▼───▼─┐ ┌──▼─────┐
   │  Issue    │ │Cycle │ │Module│ │ State  │
   │ (central)│ │      │ │      │ │        │
   └────┬─────┘ └──┬───┘ └──┬───┘ └────────┘
        │          │        │
        │    CycleIssue  ModuleIssue
        │      (M2M)       (M2M)

   ┌────┼────┬──────────┬─────────┐
   │    │    │          │         │
 Comment Activity  Reaction  Attachment

Base Model Pattern — all workspace models inherit:

FieldPurpose
idUUID primary key
created_atAuto-set on creation
updated_atAuto-set on every save
created_byForeign key to User
updated_byForeign key to User
deleted_atSoft-delete timestamp (null = active)
workspaceForeign key enforcing multi-tenancy

Key Design Patterns

Multi-Tenancy

Every query is scoped to a workspace — enforced at the view level so no data leaks between organizations.

Soft Deletion

Records are never physically deleted. deleted_at is set, and a daily Celery beat task permanently removes records older than HARD_DELETE_AFTER_DAYS.

Activity Audit Trail

Every change to an Issue generates an IssueActivity record tracking what changed, who changed it, and when.

Community / Enterprise Gating

The web app has ce/ (community edition) and ee/ (enterprise edition) directories, enabling feature gating between the free and paid versions.

Real-Time Collaboration

The Live server (apps/live/) runs Hocuspocus on port 3100:

ComponentTechnology
ServerHocuspocus 2.15.2 (Node.js)
CRDTYjs 13.6.20
Editor bindingy-prosemirror (Yjs ↔ ProseMirror)
Persistence@hocuspocus/extension-database + Redis
TransportWebSocket via express-ws

When users edit Pages collaboratively, the Hocuspocus server merges their changes using CRDT math, persists to the database, and broadcasts updates to all connected clients.

Performance Architecture

Database

  • Indexes on frequently queried fields (workspace_id, project_id, state_id)
  • Read replica support for horizontal scaling

API

  • Valkey caching for frequently accessed data
  • Throttling at multiple levels (anon, API key, asset, email verification)
  • GZip middleware for response compression

Frontend

  • MobX computed values — derived state only recalculates when dependencies change
  • Code splitting — lazy-loaded routes via React Router
  • SWR caching — stale-while-revalidate pattern for API data

What's Next