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 ace/path in the source, anee/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:
- 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_URLorCORS_ALLOWED_ORIGINSbreaks the whole thing even if every container is "up". - 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.appvsplane.api) with different middleware and auth.
2. Architecture
2.1 Service topology
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
migratoris 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 matchingVITE_*_BASE_PATHbuild-time variable produces broken static asset paths (community issue #5200 — "APP_BASE_URL does not change static paths"). The frontends bakeVITE_*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/Endpointsubclasses), so they do not appear in naive route scrapes. The authoritative endpoint list is theurlpatternsinapps/api/plane/api/urls/(external) andapps/api/plane/app/urls/(web). See Section 6.
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:
bgworkerconsumes tasks (plane/bgtasks/).beatworkerschedules periodic tasks viadjango-celery-beat(e.g. hard-delete of soft-deleted records afterHARD_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.0for CVE fixes,requests==2.33.0for 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:
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:
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), then re-run and choose Start.
Tip
The installer's
Backup Dataaction and the bundledrestore.sh/restore-airgapped.shscripts (indeployments/cli/community) are your supported backup/restore path. There's alsomigration-0.13-0.14.shfor 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_limitvia env may not override the default if the chart's values aren't wired correctly (community issue #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:
docker compose -f docker-compose-local.yml up -d
The monorepo build is orchestrated by Turborepo:
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_emailmanagement command (see Section 9).
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_*, andANALYTICS_*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 returnGiven 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) → HTTP429when exceeded. - Pagination: Cursor-based (
cursor,per_pagequery 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
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"}'
{"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). 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). SetWEB_URLcorrectly 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
ROLEenum), 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:
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=0in production. - Terminate TLS at Caddy (
CERT_EMAILfor Let's Encrypt) or at your own load balancer withTRUSTED_PROXIESset appropriately.
8.5 Hardening checklist
- [ ]
DEBUG=0 - [ ] Strong
SECRET_KEYandLIVE_SERVER_SECRET_KEY - [ ] Non-default Postgres / RabbitMQ / MinIO credentials
- [ ]
WEB_URLandCORS_ALLOWED_ORIGINSpinned to your domain (not*origins) - [ ] TLS enabled (
CERT_EMAILset, or external TLS +TRUSTED_PROXIES) - [ ] Telemetry disabled if undesired (
POSTHOG_*,SCOUT_*,ANALYTICS_*unset) - [ ] Webhook allowlists minimal; SSRF protection intact
- [ ]
ENABLE_SIGNUP=0for 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
# 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
spacecontainer's healthcheck has failed whencurlis missing from the image (community issue #9196). Ifspacereports unhealthy but serves traffic, verify the healthcheck command rather than assuming the service is down.
10.2 Backup
Back up three stores together:
- PostgreSQL — all relational data.
- Object storage (MinIO/S3) — file attachments.
plane.env— your configuration and secrets.
# 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
./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-migratorcompleted (check its logs; it should exit 0). - [ ]
apistarts 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_WORKERSfor 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 byplane/middlewareDB 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
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 andreact-router.config.ts, notNEXT_PUBLIC_*). - State is managed with MobX stores (
apps/web/core/store/), organized by domain (issue, cycle, module, workspace, member, …), withce/eeoverrides for edition-specific behavior. - API access goes through
@plane/servicesandapps/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) andplane.api(API key) are deliberately separate. Add public-API endpoints underplane/api/, notplane/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). |
# 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(andCORS_ALLOWED_ORIGINS) inplane.env; restart. - Prevention: Treat
WEB_URLas mandatory in every environment.
Static assets 404 after changing a base path
- Cause:
VITE_*_BASE_PATHis build-time; changingAPP_BASE_PATHat runtime doesn't update the bundled asset paths (issue #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_URLmismatch (issue #5853). - Solution: Align the provider's callback URL with your public domain/port and
WEB_URL. - Prevention: Configure
WEB_URLbefore registering OAuth apps.
Helm: api_key_rate_limit override ignored
- Cause: Chart values not propagating the env var (issue #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). - 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
migratorcontainer failed or was skipped. - Solution: Re-run migrations (
docker exec api python manage.py migrate) and checkplane-migratorlogs. - 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 withtest_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). A runtime-resolved base path would remove a common self-host footgun. - Healthcheck portability. The
spacehealthcheck assumedcurl(#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); chart values deserve an integration test.
- Edge-case crashes. Archiving a cycle with no end date (#9169); Safari/iOS
requestIdleCallbackuse without a guard (#9134); special characters in project name/identifier (#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_ENGINEare 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); 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
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
./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) |