Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Plane Admin Guide

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.

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

                          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 — "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.

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.

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 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). 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_email management 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_*, 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

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). 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:

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.

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

# 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). 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.
# 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-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

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).
# 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).
  • 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).
  • 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).
  • 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 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). A runtime-resolved base path would remove a common self-host footgun.
  • Healthcheck portability. The space healthcheck assumed curl (#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 requestIdleCallback use 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_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); 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)