# Plane Admin Guide

This guide is the operational and architectural reference for **Plane Community Edition** — the open-source project management platform in the `makeplane/plane` repository. It is written for the people who deploy, operate, secure, integrate with, and extend a self-hosted Plane instance: administrators, DevOps/SRE, platform engineers, and developers.

It covers the system architecture, every deployment method, the complete environment-variable reference, the REST API, authentication and SSO, security model, day-2 operations (backup/restore/upgrade), the management-command toolbox, and the internal structure of the codebase.

> [!NOTE]
> This repository is the **Community Edition (CE)**. Plane also has paid/enterprise tiers built as an overlay (`ee/` directories and a separate enterprise distribution). Configuration and features described here are CE unless explicitly marked. Where you see a `ce/` path in the source, an `ee/` sibling typically exists in non-CE builds.

---

## 1. Executive Summary

Plane is a **multi-service web application**, not a single process. A self-hosted instance is a small fleet of containers coordinated by Docker Compose (or Swarm/Kubernetes), fronted by a Caddy reverse proxy that terminates TLS.

| Layer | Technology | Source |
|---|---|---|
| Reverse proxy / TLS | **Caddy 2.11** (custom build with DNS plugins + L4) | `apps/proxy` |
| Web app (primary UI) | **React Router + Vite** SPA (TypeScript) | `apps/web` |
| Instance admin ("God mode") | React Router + Vite SPA | `apps/admin` |
| Public "Spaces" (published views) | React Router + Vite SPA | `apps/space` |
| Real-time collaboration | **Node.js + TypeScript** (Hocuspocus-style live server) | `apps/live` |
| API / business logic | **Django 4.2 + Django REST Framework 3.15** | `apps/api` |
| Background jobs | **Celery 5.4** worker + beat scheduler | `apps/api` |
| Database | **PostgreSQL 15** | container `plane-db` |
| Cache / sessions / channels | **Redis / Valkey 7.2** | container `plane-redis` |
| Message broker | **RabbitMQ 3.13** (AMQP) | container `plane-mq` |
| Object storage | **MinIO** (S3-compatible) or external **AWS S3** | container `plane-minio` |

**Two truths to internalize before operating Plane:**

1. **It is a distributed app.** The API, three frontends, the live server, the worker, the beat scheduler, and the migrator are *separate* containers that all read the same environment. A misconfigured `WEB_URL` or `CORS_ALLOWED_ORIGINS` breaks the whole thing even if every container is "up".
2. **There are two distinct APIs.** A session-authenticated **web API** (`/api/`) used by the frontends, and an API-key-authenticated **external API** (`/api/v1/`) for integrations. They live in different Django apps (`plane.app` vs `plane.api`) with different middleware and auth.

---

## 2. Architecture

### 2.1 Service topology

```text
                          Internet
                             │
                    ┌────────▼─────────┐
                    │   proxy (Caddy)  │  :80 / :443  — auto TLS (Let's Encrypt)
                    │  Caddyfile.ce    │  routes by path
                    └───┬───┬───┬───┬──┘
        /  ───────────► web │   │   │ space ◄─────── /spaces
        /god-mode ───► admin    │   │
        /live (ws) ─────────► live  │
        /api, /api/v1 ──────────► api (Django :8000, gunicorn/uvicorn)
                                     │
            ┌────────────────────────┼───────────────────────────┐
            ▼            ▼            ▼            ▼               ▼
        plane-db     plane-redis  plane-mq     plane-minio   (worker / beat)
        Postgres15   Valkey7.2    RabbitMQ    MinIO (S3)      Celery
```

### 2.2 The container roster

From `docker-compose.yml`, a full instance runs these services:

| Container | Role | Entry command |
|---|---|---|
| `web` | Main app SPA | served static behind proxy |
| `admin` | God-mode admin SPA | served static behind proxy |
| `space` | Public published-views SPA | served static behind proxy |
| `api` | Django REST API (gunicorn/uvicorn :8000) | `./bin/docker-entrypoint-api.sh` |
| `bgworker` | Celery worker (async tasks) | `./bin/docker-entrypoint-worker.sh` |
| `beatworker` | Celery beat (scheduled tasks) | `./bin/docker-entrypoint-beat.sh` |
| `plane-migrator` | Runs DB migrations once on deploy (`restart: no`) | `./bin/docker-entrypoint-migrator.sh` |
| `plane-live` | Node real-time collaboration server | `apps/live` |
| `plane-db` | PostgreSQL 15.7 (`max_connections=1000`) | `postgres` |
| `plane-redis` | Valkey 7.2 (Redis-compatible) | `valkey` |
| `plane-mq` | RabbitMQ 3.13 (management-alpine) | `rabbitmq` |
| `plane-minio` | MinIO object store (console on :9090) | `minio server /export` |
| `proxy` | Caddy reverse proxy + TLS | `caddy` |

> [!IMPORTANT]
> The `migrator` is a **run-once** container (`restart: no`). It applies database migrations and exits. After an upgrade, confirm it ran to completion before declaring the deployment healthy — an API that starts against an un-migrated database will throw schema errors.

### 2.3 Request routing

The Caddy `Caddyfile.ce` routes by path prefix:

| Path | Goes to | Base-path env |
|---|---|---|
| `/` | `web` (main app) | `APP_BASE_PATH` (default `/`) |
| `/god-mode/` | `admin` | `ADMIN_BASE_PATH` (default `/god-mode/`) |
| `/spaces/` | `space` | `SPACE_BASE_PATH` (default `/spaces/`) |
| `/live/` | `live` (WebSocket) | `LIVE_BASE_PATH` (default `/live/`) |
| `/api/`, `/api/v1/` | `api` | — |

> [!WARNING]
> Changing a base path (e.g. `APP_BASE_PATH`) without aligning the proxy config and the matching `VITE_*_BASE_PATH` build-time variable produces broken static asset paths (community issue [#5200](https://github.com/makeplane/plane/issues/5200) — "APP_BASE_URL does not change static paths"). The frontends bake `VITE_*` values at build time; runtime env alone is not enough if you change paths.

### 2.4 The two APIs

| | Web App API | External API |
|---|---|---|
| Django app | `plane.app` | `plane.api` |
| Mount | `/api/` | `/api/v1/` |
| Auth | Session cookie (CSRF-aware) | `X-API-Key` header |
| Middleware | `plane/app/middleware/api_authentication.py` | `plane/api/middleware/api_authentication.py` |
| URL definitions | `apps/api/plane/app/urls/*.py` | `apps/api/plane/api/urls/*.py` |
| Consumer | The Plane frontends | Your integrations/scripts |

> [!NOTE]
> DRF endpoints in Plane are **class-based** (`APIView`/`Endpoint` subclasses), so they do not appear in naive route scrapes. The authoritative endpoint list is the `urlpatterns` in `apps/api/plane/api/urls/` (external) and `apps/api/plane/app/urls/` (web). See [Section 6](#6-rest-api-reference).

### 2.5 Background processing

Asynchronous work (email delivery, webhook dispatch, exports, notifications, importers, cleanup) runs through **Celery** with **RabbitMQ** as the broker and **Redis** for results/locks:

- `bgworker` consumes tasks (`plane/bgtasks/`).
- `beatworker` schedules periodic tasks via `django-celery-beat` (e.g. hard-delete of soft-deleted records after `HARD_DELETE_AFTER_DAYS`).

### 2.6 Real-time collaboration

`apps/live` is a standalone Node/TypeScript service that powers **collaborative page editing** and live presence. It authenticates clients against the API using a shared secret (`LIVE_SERVER_SECRET_KEY`) and uses Redis for coordination. If `live` is down, pages still load but lose real-time co-editing.

---

## 3. Requirements

### 3.1 Minimum host

| Resource | Minimum | Notes |
|---|---|---|
| CPU | 2 vCPU | More for >50 active users. |
| RAM | 4 GB | Postgres + Redis + RabbitMQ + 8 app containers are memory-hungry. |
| Disk | 20 GB+ | Grows with attachments (object store) and DB. |
| OS | Ubuntu / Linux AMI / macOS | Windows requires Git Bash for the installer. |
| Docker | Current Docker Engine + Compose v2 | `curl -fsSL https://get.docker.com \| sh -` |

### 3.2 Software versions (pinned)

Backend (`apps/api/requirements/base.txt`):

| Component | Version |
|---|---|
| Django | 4.2.30 |
| djangorestframework | 3.15.2 |
| psycopg (Postgres driver) | 3.3.0 |
| redis / django-redis | 5.0.4 / 5.4.0 |
| celery / django-celery-beat | 5.4.0 / 2.6.0 |
| channels (WebSockets) | 4.1.0 |
| boto3 / django-storages | 1.34.96 / 1.14.2 |
| drf-spectacular (OpenAPI) | 0.28.0 |
| cryptography | 46.0.7 |
| nh3 (HTML sanitizer) | 0.2.18 |
| PyJWT | 2.12.0 |

Frontend/monorepo:

| Component | Version |
|---|---|
| Node.js | ≥ 22.18.0 |
| Package manager | pnpm 11.3.0 |
| Build orchestrator | Turborepo |
| Linter / formatter | oxlint / oxfmt |

> [!NOTE]
> Several dependency pins are **security-driven** and called out in the requirements file (e.g. `urllib3>=2.7.0` for CVE fixes, `requests==2.33.0` for the TLS-context adapter hook used in SSRF IP pinning). Do not downgrade them.

---

## 4. Deployment

Plane ships four deployment topologies under `deployments/`, all Community Edition:

| Method | Directory | Best for |
|---|---|---|
| **Docker Compose** | `deployments/cli/community` | Single-host self-host (recommended start). |
| **Docker Swarm** | `deployments/swarm/community` | Multi-node with replicas. |
| **Kubernetes (Helm)** | `deployments/kubernetes/community` | Cluster deployments (`plane-ce` chart). |
| **All-in-one (AIO)** | `deployments/aio/community` | Single container bundling all services. |

### 4.1 Docker Compose (recommended)

The official release installer drives Compose deployments:

```bash
mkdir plane-selfhost && cd plane-selfhost
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
chmod +x setup.sh
./setup.sh
```

The interactive menu:

```text
1) Install (x86_64)   2) Start   3) Stop   4) Restart
5) Upgrade   6) View Logs   7) Backup Data   8) Exit
```

Choosing **Install** creates a `plane-app/` directory with `docker-compose.yaml` and `plane.env`. Edit `plane.env` (see [Section 5](#5-configuration-reference)), then re-run and choose **Start**.

> [!TIP]
> The installer's `Backup Data` action and the bundled `restore.sh` / `restore-airgapped.sh` scripts (in `deployments/cli/community`) are your supported backup/restore path. There's also `migration-0.13-0.14.sh` for that specific version jump.

### 4.2 Docker Swarm

Use `swarm.sh` (downloaded the same way as `setup.sh` but from the swarm release asset). It supports per-service **replicas** — set `WEB_REPLICAS`, `API_REPLICAS`, `WORKER_REPLICAS`, `LIVE_REPLICAS`, etc. in the env to scale horizontally.

### 4.3 Kubernetes (Helm)

The community Helm chart is published as **`plane-ce`** on Artifact Hub (`makeplane` repository). It maps the same services to Deployments/StatefulSets.

> [!WARNING]
> A known Helm gotcha: setting `api_key_rate_limit` via env may not override the default if the chart's values aren't wired correctly (community issue [#9034](https://github.com/makeplane/plane/issues/9034)). Verify rate-limit overrides actually take effect after a Helm upgrade by checking the running API container's environment.

### 4.4 All-in-one (AIO)

`deployments/aio/community` (with `build.sh` / `start.sh` and `Caddyfile.aio.ce`) bundles every service into a single container for the simplest possible footprint. Convenient for demos and small instances; less flexible for scaling than Compose/Swarm/K8s.

### 4.5 Local development

For contributors, `setup.sh` (repo root) copies every per-service `.env.example` to `.env` (root, `web`, `api`, `space`, `admin`, `live`), generates a Django `SECRET_KEY`, runs `pnpm install`, and then you start with:

```bash
docker compose -f docker-compose-local.yml up -d
```

The monorepo build is orchestrated by Turborepo:

```bash
pnpm dev      # turbo run dev (all apps in watch mode)
pnpm build    # turbo run build
pnpm check    # lint + types + format across the workspace
```

---

## 5. Configuration Reference

Configuration is entirely **environment-variable driven**. In production the release installer writes a single flat `plane.env` (template: `deployments/cli/community/variables.env`). In local dev, configuration is split across per-app `.env` files. Django reads variables in `apps/api/plane/settings/` (`common.py`, `production.py`, `local.py`, `storage.py`).

> [!CAUTION]
> Frontend variables (`VITE_*`) are **build-time** in the SPAs. Changing them requires rebuilding the corresponding image, not just restarting it. Backend variables (everything else) are read at container start.

### 5.1 Core / web addressing

| Variable | Default | Purpose |
|---|---|---|
| `WEB_URL` | *(none — required)* | Public URL of the instance. **Mandatory.** API raises `ImproperlyConfigured: APP_BASE_URL or WEB_URL is not set` without it. |
| `APP_BASE_URL` | `None` | Base URL of the web app. |
| `APP_BASE_PATH` | `/` | Path prefix for the web app. |
| `ADMIN_BASE_URL` | `None` | God-mode admin URL. |
| `ADMIN_BASE_PATH` | `/god-mode/` | God-mode path prefix. |
| `SPACE_BASE_URL` | `None` | Public Spaces URL. |
| `SPACE_BASE_PATH` | `/spaces/` | Spaces path prefix. |
| `LIVE_BASE_URL` | `None` | Live server URL. |
| `LIVE_BASE_PATH` | `/live/` | Live server path prefix. |
| `CORS_ALLOWED_ORIGINS` | *(none)* | Comma-separated allowed origins. Must include your `WEB_URL`. |
| `ALLOWED_HOSTS` | `*` | Django allowed hosts. |
| `DEBUG` | `0` | **Set to `0` in production.** |
| `LISTEN_HTTP_PORT` / `LISTEN_HTTPS_PORT` | `80` / `443` | Proxy listen ports. |

### 5.2 Database (PostgreSQL)

| Variable | Default | Purpose |
|---|---|---|
| `DATABASE_URL` | — | Full DSN; overrides the discrete vars if set. |
| `POSTGRES_HOST` | `plane-db` | DB host. |
| `POSTGRES_PORT` | `5432` | DB port. |
| `POSTGRES_DB` | `plane` | Database name. |
| `POSTGRES_USER` / `POSTGRES_PASSWORD` | `plane` / `plane` | Credentials — **change for production.** |
| `ENABLE_READ_REPLICA` | `0` | Enable a read replica. |
| `DATABASE_READ_REPLICA_URL` / `POSTGRES_READ_REPLICA_*` | — | Read-replica connection settings. |

### 5.3 Redis, RabbitMQ, Celery

| Variable | Default | Purpose |
|---|---|---|
| `REDIS_URL` | — | Full Redis DSN (overrides host/port). |
| `REDIS_HOST` / `REDIS_PORT` | `plane-redis` / `6379` | Redis connection. |
| `AMQP_URL` | — | Full AMQP DSN (overrides discrete vars). |
| `RABBITMQ_HOST` / `RABBITMQ_PORT` | `localhost` / `5672` | Broker host/port. |
| `RABBITMQ_USER` / `RABBITMQ_PASSWORD` | `guest` / `guest` | Broker credentials — **change for production.** |
| `RABBITMQ_VHOST` | `/` | Broker vhost. |
| `GUNICORN_WORKERS` | `2` | API worker processes. |

### 5.4 Object storage (MinIO / S3)

| Variable | Default | Purpose |
|---|---|---|
| `USE_MINIO` | `0` | `1` to use the bundled MinIO; `0` for external S3. |
| `AWS_S3_ENDPOINT_URL` | `http://plane-minio:9000` | S3 endpoint. |
| `AWS_S3_BUCKET_NAME` | `uploads` | Bucket for uploads. |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | `access-key` / `secret-key` | Credentials — **change for production.** |
| `AWS_REGION` | `""` | Region for external S3. |
| `MINIO_ENDPOINT_URL` | `None` | MinIO endpoint override. |
| `MINIO_ENDPOINT_SSL` | `0` | Force HTTPS for SSL termination. |
| `FILE_SIZE_LIMIT` | `5242880` (5 MB) | Max upload size in bytes. |
| `SIGNED_URL_EXPIRATION` | `3600` | Presigned download/upload URL lifetime (seconds). |
| `HARD_DELETE_AFTER_DAYS` | `60` | Days before soft-deleted records/files are purged. |

### 5.5 Security & sessions

| Variable | Default | Purpose |
|---|---|---|
| `SECRET_KEY` | *(none — required)* | Django secret. Generate a strong random value. |
| `LIVE_SERVER_SECRET_KEY` | *(none)* | Shared secret between API and live server. **Set a secure value in production.** |
| `SESSION_COOKIE_NAME` | `session-id` | Session cookie name. |
| `SESSION_COOKIE_AGE` | `604800` (7 days) | Session lifetime. |
| `ADMIN_SESSION_COOKIE_AGE` | `3600` | God-mode session lifetime. |
| `COOKIE_DOMAIN` | `None` | Cookie domain. |
| `API_KEY_RATE_LIMIT` | `60/minute` | External API rate limit per token. |
| `AUTHENTICATION_RATE_LIMIT` | `10/minute` | Per-IP throttle on anonymous auth endpoints (magic-link/sign-in/sign-up). |
| `WEBHOOK_ALLOWED_IPS` | — | CIDR/IP allowlist for webhook targets that resolve to private networks. |
| `WEBHOOK_ALLOWED_HOSTS` | — | Hostname allowlist bypassing the private-IP SSRF check. |
| `WEBHOOK_DISALLOWED_DOMAINS` | — | Explicitly blocked webhook domains. |

### 5.6 TLS / proxy (Caddy)

| Variable | Default | Purpose |
|---|---|---|
| `SITE_ADDRESS` | `:80` | Caddy site address. |
| `CERT_EMAIL` | — | Email for Let's Encrypt; enables automatic HTTPS. |
| `CERT_ACME_CA` | Let's Encrypt prod | ACME directory URL. |
| `CERT_ACME_DNS` | — | DNS-challenge config (provider + API key) for wildcard/closed-port certs. |
| `TRUSTED_PROXIES` | `0.0.0.0/0` | Trusted upstream proxies. |

### 5.7 Email (SMTP)

Configured via env or, preferably, in **God mode** (stored in the instance config; see `license/utils/instance_value.py`):

| Variable | Default |
|---|---|
| `EMAIL_HOST` | — |
| `EMAIL_HOST_USER` / `EMAIL_HOST_PASSWORD` | — |
| `EMAIL_PORT` | `587` |
| `EMAIL_USE_TLS` / `EMAIL_USE_SSL` | `1` / `0` |
| `EMAIL_FROM` | `Team Plane <[email removed]>` |
| `EMAIL_BACKEND` | console backend (dev) |

> [!IMPORTANT]
> Without SMTP, magic-link login emails and member invitations will not send. Configure email before onboarding a team. Test with the `test_email` management command (see [Section 9](#9-management-commands)).

### 5.8 Authentication providers

| Variable | Purpose |
|---|---|
| `ENABLE_SIGNUP` (`1`) | Allow open sign-ups. |
| `ENABLE_MAGIC_LINK_LOGIN` (`1`) | Allow passwordless magic-link login. |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google OAuth. |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | GitHub OAuth. |
| `GITLAB_HOST` / `GITLAB_CLIENT_ID` / `GITLAB_CLIENT_SECRET` | GitLab OAuth. |
| `GITEA_HOST` / `GITEA_CLIENT_ID` / `GITEA_CLIENT_SECRET` | Gitea OAuth. |

### 5.9 AI, analytics & integrations

| Variable | Purpose |
|---|---|
| `OPENAI_API_KEY` / `OPENAI_API_BASE` / `GPT_ENGINE` | AI features (**deprecated** keys; AI config is moving to God mode). |
| `UNSPLASH_ACCESS_KEY` | Cover-image search. |
| `POSTHOG_API_KEY` / `POSTHOG_HOST` | Product analytics (telemetry). |
| `ANALYTICS_SECRET_KEY` / `ANALYTICS_BASE_API` | Analytics service. |
| `SCOUT_MONITOR` / `SCOUT_KEY` | Scout APM performance monitoring. |
| `OTLP_ENDPOINT` | OpenTelemetry trace export. |
| `ENABLE_DRF_SPECTACULAR` (`0`) | Expose the OpenAPI schema/Swagger UI. Set `1` to enable. |
| `INSTANCE_CHANGELOG_URL` | In-app changelog source. |

> [!TIP]
> For a privacy-hardened instance, leave `POSTHOG_*`, `SCOUT_*`, and `ANALYTICS_*` unset to disable outbound telemetry. None are required for core functionality.

---

## 6. REST API Reference

Plane exposes a stable, documented **external API** under `/api/v1/`, authenticated with the `X-API-Key` header (tokens created in **Workspace Settings → API Tokens**).

### 6.1 Conventions

- **Base path:** `/api/v1/`
- **Auth:** `X-API-Key: <token>` (middleware: `plane/api/middleware/api_authentication.py`; invalid tokens return `Given API token is not valid`).
- **Scoping:** Most resources are nested under `workspaces/<slug>/projects/<project_id>/...`.
- **Rate limit:** `API_KEY_RATE_LIMIT` (default 60/min) → HTTP `429` when exceeded.
- **Pagination:** Cursor-based (`cursor`, `per_page` query params).
- **IDs:** UUIDs for resources; workspaces are addressed by **slug**.

### 6.2 Endpoint catalogue

Derived from `apps/api/plane/api/urls/`. `{slug}` = workspace slug, `{project_id}` and other IDs are UUIDs.

**Projects** (`project.py`)

| Method | Path |
|---|---|
| GET, POST | `workspaces/{slug}/projects/` |
| GET, PATCH, DELETE | `workspaces/{slug}/projects/{pk}/` |
| POST, DELETE | `workspaces/{slug}/projects/{project_id}/archive/` |
| GET | `workspaces/{slug}/projects/{project_id}/summary/` |

**Work Items / Issues** (`work_item.py`) — list/create/detail plus sub-resources (comments, links, attachments, activity).

| Method | Path |
|---|---|
| GET, POST | `workspaces/{slug}/projects/{project_id}/issues/` |
| GET, PATCH, DELETE | `workspaces/{slug}/projects/{project_id}/issues/{pk}/` |

**Cycles** (`cycle.py`)

| Method | Path |
|---|---|
| GET, POST | `workspaces/{slug}/projects/{project_id}/cycles/` |
| GET, PATCH, DELETE | `workspaces/{slug}/projects/{project_id}/cycles/{pk}/` |
| GET, POST | `…/cycles/{cycle_id}/cycle-issues/` |
| GET, DELETE | `…/cycles/{cycle_id}/cycle-issues/{issue_id}/` |
| POST | `…/cycles/{cycle_id}/transfer-issues/` |
| POST | `…/cycles/{cycle_id}/archive/` |
| GET | `…/archived-cycles/` |
| DELETE | `…/archived-cycles/{cycle_id}/unarchive/` |

**Modules** (`module.py`)

| Method | Path |
|---|---|
| GET, POST | `workspaces/{slug}/projects/{project_id}/modules/` |
| GET, PATCH, DELETE | `…/modules/{pk}/` |
| GET, POST | `…/modules/{module_id}/module-issues/` |
| DELETE | `…/modules/{module_id}/module-issues/{issue_id}/` |
| POST | `…/modules/{pk}/archive/` |
| GET | `…/archived-modules/` |
| DELETE | `…/archived-modules/{pk}/unarchive/` |

**States** (`state.py`), **Labels** (`label.py`)

| Method | Path |
|---|---|
| GET, POST | `…/projects/{project_id}/states/` |
| GET, PATCH, DELETE | `…/projects/{project_id}/states/{pk}/` |
| GET, POST | `…/projects/{project_id}/labels/` |
| GET, PATCH, DELETE | `…/projects/{project_id}/labels/{pk}/` |

**Estimates** (`estimate.py`)

| Method | Path |
|---|---|
| GET, POST, PATCH, DELETE | `…/projects/{project_id}/estimates/` |
| GET, POST | `…/estimates/{estimate_id}/estimate-points/` |
| PATCH, DELETE | `…/estimates/{estimate_id}/estimate-points/{estimate_point_id}/` |

**Intake** (`intake.py`)

| Method | Path |
|---|---|
| GET, POST | `…/projects/{project_id}/intake-issues/` |
| GET, PATCH, DELETE | `…/projects/{project_id}/intake-issues/{issue_id}/` |

**Members** (`member.py`)

| Method | Path |
|---|---|
| GET, POST | `…/projects/{project_id}/members/` |
| GET, PATCH, DELETE | `…/projects/{project_id}/members/{pk}/` |
| GET | `workspaces/{slug}/members/` |

**Invitations** (`invite.py`): DRF router under `workspaces/{slug}/invitations/`.

**Assets** (`asset.py`) — file uploads via presigned URLs:

| Method | Path |
|---|---|
| POST | `assets/user-assets/` |
| PATCH, DELETE | `assets/user-assets/{asset_id}/` |
| POST | `assets/user-assets/server/` |
| POST | `workspaces/{slug}/assets/` |
| GET, PATCH | `workspaces/{slug}/assets/{asset_id}/` |

**Stickies** (`sticky.py`), **User** (`user.py`): personal sticky notes and the authenticated user's profile.

### 6.3 Interactive docs (OpenAPI)

Plane generates an OpenAPI 3 schema via **drf-spectacular**. Enable it with `ENABLE_DRF_SPECTACULAR=1`, then browse:

| Path | What |
|---|---|
| `/api/v1/schema/` | Raw OpenAPI schema |
| `/api/v1/schema/swagger-ui/` | Swagger UI |
| `/api/v1/schema/redoc/` | ReDoc |

> [!NOTE]
> The schema is curated for the public API. Filtering and tagging hooks live in `apps/api/plane/utils/openapi/` (`preprocess_filter_api_v1_paths`, `postprocess_assign_tags`).

### 6.4 Example: end-to-end work-item creation

```bash
export PLANE=https://plane.example.com
export KEY="your-api-key"
export WS="my-team"

# 1. Find a project
PROJECT=$(curl -s "$PLANE/api/v1/workspaces/$WS/projects/" -H "X-API-Key: $KEY" \
  | jq -r '.results[0].id')

# 2. Create a work item
curl -s -X POST "$PLANE/api/v1/workspaces/$WS/projects/$PROJECT/issues/" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"name":"Investigate slow dashboard","priority":"high"}'
```

```text
{"id":"…","name":"Investigate slow dashboard","sequence_id":42,"priority":"high", …}
```

---

## 7. Authentication, SSO, and the data model

### 7.1 Authentication methods

Plane's auth lives in `apps/api/plane/authentication/`:

- **Credentials** (`provider/credentials/`): email + password, and passwordless **magic code** (email).
- **OAuth** (`provider/oauth/`): **Google, GitHub, GitLab, Gitea**.

Configure providers in **God mode** (or via the `configure_instance` command / env vars in [Section 5.8](#58-authentication-providers)). Toggle open sign-up with `ENABLE_SIGNUP` and magic links with `ENABLE_MAGIC_LINK_LOGIN`.

> [!CAUTION]
> OAuth redirect URIs must match your real public domain and port. A mismatch produces `invalid oauth2 redirect_uri` (community issue [#5853](https://github.com/makeplane/plane/issues/5853)). Set `WEB_URL` correctly *before* registering OAuth apps.

### 7.2 Permission model

Authorization is enforced by DRF permission classes in `apps/api/plane/app/permissions/`. Roles are hierarchical:

- **Workspace roles:** Admin, Member, Guest.
- **Project roles:** Admin, Member, Guest (the `ROLE` enum), applied per project membership.

Each endpoint checks the caller's workspace/project role before allowing reads or writes.

### 7.3 Core data model (the nouns)

The Django models in `apps/api/plane/db/models/` define the domain. Key entities:

```text
User ──< WorkspaceMember >── Workspace ──< Project ──< Issue (Work Item)
                                   │            ├──< Cycle ──< CycleIssue
                                   │            ├──< Module ──< ModuleIssue
                                   │            ├──< State (StateGroup)
                                   │            ├──< Label
                                   │            ├──< IssueView (Views)
                                   │            ├──< Page ──< PageVersion
                                   │            ├──< Estimate ──< EstimatePoint
                                   │            └──< Intake ──< IntakeIssue
                                   └──< Webhook, DeployBoard, APIToken
```

| Domain area | Representative models |
|---|---|
| Identity | `User`, `Profile`, `Account`, `Session`, `Device`, `SocialLoginConnection` |
| Org structure | `Workspace`, `WorkspaceMember`, `Project`, `ProjectMember`, `Team` |
| Work | `Issue`, `IssueType`, `DraftIssue`, `IssueComment`, `IssueAttachment`, `IssueLink`, `IssueRelation`, `IssueActivity`, `IssueReaction`, `IssueSubscriber`, `IssueVersion` |
| Planning | `Cycle`, `CycleIssue`, `Module`, `ModuleIssue`, `State`, `Label`, `Estimate`, `EstimatePoint` |
| Knowledge | `Page`, `PageVersion`, `PageLabel`, `Sticky` |
| Triage | `Intake`, `IntakeIssue`, `IntakeIssueStatus` |
| Sharing | `DeployBoard`, `ProjectDeployBoard`, `ProjectPublicMember` |
| Integration | `Webhook`, `WebhookLog`, `APIToken`, `APIActivityLog`, `Importer`, `ExporterHistory` |
| Notifications | `Notification`, `EmailNotificationLog`, `UserNotificationPreference` |

Most models inherit soft-delete and audit fields from base classes (`BaseModel`, `ProjectBaseModel`, `WorkspaceBaseModel`).

---

## 8. Security

Plane CE includes several deliberate security controls. Operators should understand and not weaken them.

### 8.1 SSRF protection on webhooks and link unfurling

Webhook targets and outbound URL fetches are validated against private/internal networks. The code (`plane/utils/ip_address.py`, `plane/utils/url_security.py`) rejects:

- Non-HTTP(S) schemes — `Invalid URL scheme. Only HTTP and HTTPS are allowed`.
- Private/internal network addresses — `Access to private/internal networks is not allowed`.
- Unresolvable hosts and excessive redirects.

To allow specific internal targets (e.g. a trusted internal service), use the **allowlists**: `WEBHOOK_ALLOWED_IPS` (CIDR/IPs) and `WEBHOOK_ALLOWED_HOSTS` (hostnames). `WEBHOOK_DISALLOWED_DOMAINS` blocks specific domains.

> [!WARNING]
> Widening these allowlists weakens SSRF protection. Only add IPs/hosts you fully trust, and never allow `0.0.0.0/0`.

### 8.2 Input and content safety

- HTML content is sanitized with **nh3** before storage/render.
- Workspace slugs are validated (`Slug is not valid`) to prevent routing abuse.
- Upload size is capped by `FILE_SIZE_LIMIT`.

### 8.3 Rate limiting

- External API: `API_KEY_RATE_LIMIT` (default 60/min) per token.
- Anonymous auth endpoints (magic link, sign-in, sign-up): `AUTHENTICATION_RATE_LIMIT` (default 10/min) per IP — a brute-force/abuse guard.

### 8.4 Secrets and transport

- Set strong, unique values for `SECRET_KEY`, `LIVE_SERVER_SECRET_KEY`, DB/RabbitMQ/MinIO credentials. The defaults (`plane`/`plane`, `access-key`/`secret-key`) are for local dev only.
- Run with `DEBUG=0` in production.
- Terminate TLS at Caddy (`CERT_EMAIL` for Let's Encrypt) or at your own load balancer with `TRUSTED_PROXIES` set appropriately.

### 8.5 Hardening checklist

- [ ] `DEBUG=0`
- [ ] Strong `SECRET_KEY` and `LIVE_SERVER_SECRET_KEY`
- [ ] Non-default Postgres / RabbitMQ / MinIO credentials
- [ ] `WEB_URL` and `CORS_ALLOWED_ORIGINS` pinned to your domain (not `*` origins)
- [ ] TLS enabled (`CERT_EMAIL` set, or external TLS + `TRUSTED_PROXIES`)
- [ ] Telemetry disabled if undesired (`POSTHOG_*`, `SCOUT_*`, `ANALYTICS_*` unset)
- [ ] Webhook allowlists minimal; SSRF protection intact
- [ ] `ENABLE_SIGNUP=0` for closed instances

---

## 9. Management Commands

Run Django management commands inside the `api` container: `docker exec -it api python manage.py <command>`.

### 9.1 Instance setup & admin

| Command | Purpose |
|---|---|
| `register_instance <machine_signature>` | Register the self-hosted instance. |
| `configure_instance` | Apply instance config (auth providers, email) from env. |
| `create_instance_admin <admin_email>` | Promote a user to **instance admin** (God mode). |
| `create_bucket` / `update_bucket` | Create/update the object-storage bucket. |

### 9.2 User & workspace operations

| Command | Purpose |
|---|---|
| `activate_user <email>` | Activate a user account. |
| `reset_password <email>` | Reset a user's password (rejects common passwords). |
| `create_project_member --project_id --user_email --role <int>` | Add a member to a project. |
| `update_deleted_workspace_slug <slug> [--dry-run]` | Repair slugs of deleted workspaces. |
| `create_dummy_data <slug>` | Seed demo data (dev/testing). |

### 9.3 Data integrity & maintenance

| Command | Purpose |
|---|---|
| `clear_cache [--key]` | Clear cached values. |
| `fix_duplicate_sequences <issue_identifier> --slug` | Repair duplicate work-item sequence numbers. |
| `copy_issue_comment_to_description` | One-off content migration. |
| `sync_issue_version` / `sync_issue_description_version` | Backfill version history records. |
| `test_email --to_email <addr>` | Send a test email to verify SMTP. |

### 9.4 Startup guards

| Command | Purpose |
|---|---|
| `wait_for_db` | Block until Postgres is reachable (used in entrypoints). |
| `wait_for_migrations` | Block until migrations are applied. |

> [!TIP]
> First-run admin bootstrap, in order: bring the stack up → `configure_instance` (loads provider/email config) → `create_instance_admin [email removed]` → sign in and open `/god-mode`.

---

## 10. Operations

### 10.1 Health and logs

```bash
# Service status
docker compose ps

# Tail logs (or use ./setup.sh → View Logs)
docker compose logs -f api worker beat-worker live
```

> [!NOTE]
> A known healthcheck issue: the `space` container's healthcheck has failed when `curl` is missing from the image (community issue [#9196](https://github.com/makeplane/plane/issues/9196)). If `space` reports unhealthy but serves traffic, verify the healthcheck command rather than assuming the service is down.

### 10.2 Backup

Back up **three** stores together:

1. **PostgreSQL** — all relational data.
2. **Object storage** (MinIO/S3) — file attachments.
3. **`plane.env`** — your configuration and secrets.

```bash
# Database dump
docker exec plane-db pg_dump -U plane plane > plane_db_$(date +%F).sql
```

The installer's **Backup Data** action and `restore.sh` / `restore-airgapped.sh` in `deployments/cli/community` are the supported, tested path — prefer them over ad-hoc scripts.

> [!CAUTION]
> A database backup alone is **not** a full backup. Attachments live in object storage. Restoring the DB without the bucket leaves work items pointing at missing files.

### 10.3 Restore

Use `restore.sh` (or `restore-airgapped.sh` for offline environments) from `deployments/cli/community`. Restore the database and the object store from the **same point in time** to avoid dangling asset references.

### 10.4 Upgrades

```bash
./setup.sh        # choose: 7) Backup Data  (always first)
./setup.sh        # choose: 5) Upgrade
```

The upgrade pulls new images and runs the **migrator** container. Verify:

- [ ] `plane-migrator` completed (check its logs; it should exit 0).
- [ ] `api` starts cleanly (no schema errors).
- [ ] Frontends load and the version updates.

Version-specific migration helpers (e.g. `migration-0.13-0.14.sh`) exist for breaking jumps — read release notes before major upgrades.

### 10.5 Scaling

- Increase `GUNICORN_WORKERS` for API throughput.
- In Swarm/K8s, raise `*_REPLICAS` (web/api/worker/live).
- Add a Postgres **read replica** (`ENABLE_READ_REPLICA=1` + replica vars) to offload reads; routing is handled by `plane/middleware` DB routing.
- Postgres is launched with `max_connections=1000`; tune to your worker/replica count.

### 10.6 Periodic jobs

`beat-worker` runs scheduled Celery tasks (via `django-celery-beat`), including hard-deletion of soft-deleted records after `HARD_DELETE_AFTER_DAYS`. Keep the beat worker running; without it, soft-deleted data is never purged and scheduled notifications won't fire.

---

## 11. Codebase Internals (for developers)

### 11.1 Monorepo layout

```text
plane/
├── apps/
│   ├── api/      Django + DRF backend (the system of record)
│   │   └── plane/
│   │       ├── app/             Web API (session auth)  — views, urls, serializers
│   │       ├── api/             External API (X-API-Key) — views, urls, serializers
│   │       ├── db/models/       Domain models + migrations + management commands
│   │       ├── authentication/  Credentials + OAuth providers, adapters, middleware
│   │       ├── bgtasks/         Celery tasks
│   │       ├── license/         Instance registration / God-mode config
│   │       ├── settings/        common/local/production/storage settings
│   │       └── utils/           Pagination, exporters, OpenAPI, SSRF guards, analytics
│   ├── web/      Main app SPA (React Router + Vite + MobX)
│   ├── admin/    God-mode instance admin SPA
│   ├── space/    Public published-views SPA
│   ├── live/     Node/TS real-time collaboration server
│   └── proxy/    Caddy build + Caddyfiles
├── packages/     Shared TS libraries (see below)
└── deployments/  cli / swarm / kubernetes / aio  (community)
```

### 11.2 Shared packages

The frontends compose from shared workspace packages (`packages/*`), built with Turborepo:

| Package | Role |
|---|---|
| `@plane/types` | Shared TypeScript types (the cross-app contract). |
| `@plane/constants` | Shared constants, settings, endpoints. |
| `@plane/services` | API client services (one per resource). |
| `@plane/ui` / `@plane/propel` | Component libraries (design system). |
| `@plane/editor` | Rich-text editor (TipTap-based) with `ce`/`ee` extension splits. |
| `@plane/shared-state` | Cross-app state (rich filters, work-item filters). |
| `@plane/utils` | Shared helpers (rich filters, datetime, files, permissions). |
| `@plane/i18n` | Internationalization (many locales under `src/locales/`). |
| `@plane/hooks`, `@plane/logger`, `@plane/decorators` | Supporting libraries. |

### 11.3 Frontend architecture notes

- The web SPAs use **React Router + Vite** (the project migrated off Next.js — note `VITE_*` env vars and `react-router.config.ts`, not `NEXT_PUBLIC_*`).
- State is managed with **MobX** stores (`apps/web/core/store/`), organized by domain (issue, cycle, module, workspace, member, …), with `ce`/`ee` overrides for edition-specific behavior.
- API access goes through `@plane/services` and `apps/web/core/services/`.

### 11.4 The CE/EE split

Throughout the code, `ce/` directories hold Community Edition implementations and `ee/` directories (present in non-CE builds) hold enterprise overrides. The build selects one based on edition. When reading or extending CE, stay within `ce/` paths; an `ee/` import in CE will be a stub or absent.

### 11.5 Backend conventions

- **Two URL trees, two middlewares:** `plane.app` (session) and `plane.api` (API key) are deliberately separate. Add public-API endpoints under `plane/api/`, not `plane/app/`.
- **Serializers** mirror the URL split (`app/serializers/`, `api/serializers/`).
- **Soft deletes** via base models; hard deletion is a scheduled task.
- **Pagination** is cursor-based (`plane/utils/paginator.py`, `global_paginator.py`).
- **Exports** use the schema-based exporter framework (`plane/utils/exporters/`) supporting CSV/JSON/XLSX.

### 11.6 Testing

The backend test suite (`apps/api/plane/tests/`) is organized into:

| Tier | Scope |
|---|---|
| **unit** | Functions/classes in isolation. |
| **contract** | API contracts — `contract/api/` (external, `api_key_client`), `contract/app/` (web, `session_client`). |
| **smoke** | End-to-end against a live Django test server (`plane_server` fixture). |

```bash
# In apps/api
python -m pytest                       # all
./run_tests.py -u                      # unit only
./run_tests.py -c -o                   # contract + coverage
./run_tests.py -p                      # parallel
```

External services are mocked (`mock_redis`, `mock_elasticsearch`, `mock_celery`) for fast tests.

---

## 12. Troubleshooting (operator)

### Error: `ImproperlyConfigured: APP_BASE_URL or WEB_URL is not set`

- **Cause:** API started without `WEB_URL`.
- **Solution:** Set `WEB_URL` (and `CORS_ALLOWED_ORIGINS`) in `plane.env`; restart.
- **Prevention:** Treat `WEB_URL` as mandatory in every environment.

### Static assets 404 after changing a base path

- **Cause:** `VITE_*_BASE_PATH` is build-time; changing `APP_BASE_PATH` at runtime doesn't update the bundled asset paths (issue [#5200](https://github.com/makeplane/plane/issues/5200)).
- **Solution:** Rebuild the SPA images with the matching `VITE_*_BASE_PATH`, and update the Caddy route.
- **Prevention:** Decide base paths before building images.

### OAuth login fails: `invalid oauth2 redirect_uri`

- **Cause:** Redirect URI / `WEB_URL` mismatch (issue [#5853](https://github.com/makeplane/plane/issues/5853)).
- **Solution:** Align the provider's callback URL with your public domain/port and `WEB_URL`.
- **Prevention:** Configure `WEB_URL` before registering OAuth apps.

### Helm: `api_key_rate_limit` override ignored

- **Cause:** Chart values not propagating the env var (issue [#9034](https://github.com/makeplane/plane/issues/9034)).
- **Solution:** Verify the rendered Deployment actually sets `API_KEY_RATE_LIMIT`; patch values accordingly.
- **Prevention:** Confirm env after each Helm upgrade.

### `space` container marked unhealthy

- **Cause:** Healthcheck uses `curl`, which may be absent from the image (issue [#9196](https://github.com/makeplane/plane/issues/9196)).
- **Solution:** Adjust the healthcheck or add `curl`; confirm the service actually serves before alarming.
- **Prevention:** Track upstream fixes; pin known-good image tags.

### Webhook never fires / `Access to private/internal networks is not allowed`

- **Cause:** SSRF guard blocked a target resolving to a private network.
- **Solution:** Add the trusted target to `WEBHOOK_ALLOWED_IPS` / `WEBHOOK_ALLOWED_HOSTS`.
- **Prevention:** Use public, resolvable webhook endpoints, or allowlist deliberately and minimally.

### Migrations didn't apply after upgrade

- **Cause:** The `migrator` container failed or was skipped.
- **Solution:** Re-run migrations (`docker exec api python manage.py migrate`) and check `plane-migrator` logs.
- **Prevention:** Verify the migrator exits 0 as part of every upgrade.

### Emails not sending

- **Cause:** SMTP unset or using the console backend.
- **Solution:** Configure email in God mode or via `EMAIL_*` env; verify with `test_email`.
- **Prevention:** Set SMTP before onboarding users.

---

## 13. Improvements & Technical Debt (observed)

These are observations from the source and community tracker, useful for operators planning around rough edges and for contributors picking work.

- **Base-path handling is build-time-coupled.** Because the SPAs bake `VITE_*_BASE_PATH`, runtime base-path changes silently break assets ([#5200](https://github.com/makeplane/plane/issues/5200)). A runtime-resolved base path would remove a common self-host footgun.
- **Healthcheck portability.** The `space` healthcheck assumed `curl` ([#9196](https://github.com/makeplane/plane/issues/9196)). Healthchecks should use tooling guaranteed present in the image.
- **Helm value wiring.** Some env overrides (rate limit) didn't propagate through the chart ([#9034](https://github.com/makeplane/plane/issues/9034)); chart values deserve an integration test.
- **Edge-case crashes.** Archiving a cycle with no end date ([#9169](https://github.com/makeplane/plane/issues/9169)); Safari/iOS `requestIdleCallback` use without a guard ([#9134](https://github.com/makeplane/plane/issues/9134)); special characters in project name/identifier ([#8969](https://github.com/makeplane/plane/issues/8969)). These point to a need for more defensive input validation and browser-API feature detection.
- **Deprecated AI env keys.** `OPENAI_API_KEY`/`OPENAI_API_BASE`/`GPT_ENGINE` are marked deprecated in favor of God-mode-managed AI config; operators should migrate off the env keys.
- **Installer OS coverage.** The setup script has failed on some RHEL-based systems ([#9199](https://github.com/makeplane/plane/issues/9199)); broader OS testing would reduce install friction.

---

## 14. Quick Reference

### Service ports (internal)

| Service | Port |
|---|---|
| api (Django) | 8000 |
| live | 3100 |
| web / admin / space (dev) | 3000 / 3001 / 3002 |
| Postgres | 5432 |
| Redis/Valkey | 6379 |
| RabbitMQ | 5672 |
| MinIO (API / console) | 9000 / 9090 |
| Proxy | 80 / 443 |

### Must-set production variables

```bash
WEB_URL=https://plane.example.com
CORS_ALLOWED_ORIGINS=https://plane.example.com
SECRET_KEY=<50+ random chars>
LIVE_SERVER_SECRET_KEY=<random>
POSTGRES_PASSWORD=<strong>
RABBITMQ_PASSWORD=<strong>
AWS_ACCESS_KEY_ID=<strong>; AWS_SECRET_ACCESS_KEY=<strong>
CERT_EMAIL=[email removed]
DEBUG=0
```

### Common operator commands

```bash
./setup.sh                                   # install/start/stop/upgrade/backup menu
docker compose ps                            # status
docker compose logs -f api worker            # logs
docker exec api python manage.py migrate     # apply migrations
docker exec api python manage.py create_instance_admin [email removed]
docker exec api python manage.py test_email --to_email [email removed]
docker exec plane-db pg_dump -U plane plane > backup.sql
```

### API quick facts

| | |
|---|---|
| Base | `/api/v1/` |
| Auth | `X-API-Key: <token>` |
| Rate limit | `API_KEY_RATE_LIMIT` (60/min) |
| Docs | `/api/v1/schema/swagger-ui/` (needs `ENABLE_DRF_SPECTACULAR=1`) |

---

_Generated June 2026 from makeplane/plane at 0bbfe95. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
