Phase 68 β Plane CE v1.3.1 β Project Tracker
Plane CE is the open-source project management tool deployed in the plane namespace. It provides Kanban boards, issue tracking, and a full REST API consumed by the Phase 69 minicloud-plane integration service.
Why Planeβ
| Criterion | Plane CE | Linear | GitHub Projects | Jira |
|---|---|---|---|---|
| Self-hosted | β | β | β | β (Data Center only) |
| REST API | β Full | β Full | Limited | β Full |
| Webhook support | β HMAC-signed | β | β | β |
| OIDC (Authentik) | β Pro only | β | β | β |
| Homelab resource fit | ~1 GiB RAM | N/A | N/A | ~8 GiB RAM |
| Portfolio demonstrability | Visible via API | Vendor-locked | Vendor-locked | Vendor-locked |
The critical limitation: OIDC is Plane Pro only. Plane CE supports Google, GitHub, GitLab, and Gitea OAuth but not a generic OIDC provider like Authentik. Authentication is email + password with TOTP optional. An Authentik OIDC provider (pk=13) exists as a placeholder for a future Plane Pro upgrade.
Architectureβ
plane namespace
βββ plane-web (Next.js frontend β SSR)
βββ plane-space (public board view)
βββ plane-admin (admin panel)
βββ plane-api (Django REST API β plane.api module, /api/v1/)
βββ plane-worker (Celery background tasks β webhooks, emails)
βββ plane-beat (Celery beat scheduler)
βββ plane-live (WebSocket live collaboration)
βββ plane-postgres (StatefulSet, 5 GiB Longhorn PVC)
βββ plane-redis (StatefulSet, 500 MiB Longhorn PVC)
βββ plane-rabbitmq (StatefulSet, 500 MiB Longhorn PVC β task queue)
βββ plane-minio (StatefulSet, 10 GiB Longhorn PVC β file uploads)
Ingress: https://plane.devandre.sbs (Cloudflare Tunnel, public) and https://plane.10.0.0.200.nip.io (internal).
Secrets: All secrets (Postgres password, Redis URL, RabbitMQ credentials, MinIO keys, SECRET_KEY) sourced from Vault via External Secrets Operator β plane-app-secrets, plane-pgdb-secrets, etc.
Key Gotcha β Plane Has Two Django Modulesβ
This is the most important architectural detail for API consumers:
| Module | URL prefix | Auth header | Auth value |
|---|---|---|---|
plane.app | /api/ | Authorization | Bearer <session-token> (browser) |
plane.api | /api/v1/ | X-Api-Key | <api-token> |
Everything reachable via API key (the standard integration path) is under /api/v1/. Using /api/ with an API key returns 401 or 403. Using X-Api-Token (a common guess) also returns 401 β the header is X-Api-Key.
Source: /code/plane/app/middleware/api_authentication.py, auth_header_name = "X-Api-Key".
Workspace Bootstrap β Criticalβ
Plane CE does not auto-create a workspace. The onboarding flow (workspace name, invite, plan selection) must be completed in the browser at first login, OR via the Django shell. The API returns 403 on all workspace endpoints until a Workspace row and corresponding WorkspaceMember row exist in the database.
If onboarding was skipped (common in automated setups), bootstrap via Django shell:
# On any plane-api pod
kubectl exec -n plane -it deploy/plane-api -- python manage.py shell
from plane.db.models import Workspace, WorkspaceMember, User
owner = User.objects.get(email="your@email.com")
ws = Workspace.objects.create(
name="andrelair",
slug="andrelair",
owner=owner,
)
WorkspaceMember.objects.create(
workspace=ws,
member=owner,
role=20, # 20 = Admin
)
print(ws.id) # save this UUID
API Token Creationβ
API tokens (for the X-Api-Key header) are not exposed in the CE web UI. Create them via Django shell:
kubectl exec -n plane -it deploy/plane-api -- python manage.py shell
from plane.db.models import APIToken, User, Workspace
user = User.objects.get(email="your@email.com")
ws = Workspace.objects.first()
token = APIToken.objects.create(
user=user,
workspace=ws,
label="minicloud-plane-integration",
is_service=True,
)
print(token.token)
Store the token in Vault: vault kv put secret/platform/minicloud-plane PLANE_TOKEN=<token>.
SSRF Protection β WEBHOOK_ALLOWED_HOSTSβ
Plane CE's validate_url() function blocks webhook delivery to private IPs (RFC1918) and internal cluster hostnames. This is intentional SSRF protection but breaks delivery to internal services.
The escape hatch is the WEBHOOK_ALLOWED_HOSTS environment variable β a comma-separated list of hostnames that bypass IP validation.
# helm-values/plane-values.yaml
env:
webhook_allowed_hosts: "minicloud-plane.minicloud-plane-dev.svc.cluster.local"
The Plane Helm chart maps snake_case env keys β UPPER_SNAKE_CASE environment variables. After updating helm-values/plane-values.yaml, ArgoCD syncs and the worker pods pick up the new env.
Source: /code/plane/utils/ip_address.py.
Helm Values (minicloud-gitops)β
# helm-values/plane-values.yaml
planeVersion: "v1.3.1"
ingress:
enabled: true
appHost: "plane.devandre.sbs"
env:
cors_allowed_origins: "https://plane.devandre.sbs,https://plane.10.0.0.200.nip.io"
webhook_allowed_hosts: "minicloud-plane.minicloud-plane-dev.svc.cluster.local"
# All secrets from Vault via ESO
external_secrets:
app_env_existingSecret: "plane-app-secrets"
pgdb_existingSecret: "plane-pgdb-secrets"
rabbitmq_existingSecret: "plane-rabbitmq-secrets"
doc_store_existingSecret: "plane-doc-store-secrets"
live_env_existingSecret: "plane-live-secrets"
Current Projectsβ
| Project | Identifier | UUID |
|---|---|---|
| Platform Tasks | PT | 870d8973-8525-4d5d-9bab-43a4d6a517f1 |
Create new projects at https://plane.devandre.sbs or via the minicloud-plane REST API once authenticated.
Quick Health Checkβ
# Pod status
kubectl get pods -n plane
# API reachability (from controller)
curl -s -H "X-Api-Key: <token>" \
http://plane-api.plane.svc.cluster.local/api/v1/workspaces/ | jq '.[].name'
# Webhook delivery status (Celery worker logs)
kubectl logs -n plane -l app=worker --tail=20
Accessβ
- URL: https://plane.devandre.sbs (public, no Tailscale needed)
- Login: email + password (admin creds at
~/.plane-adminon controller) - Admin panel: https://plane.devandre.sbs/god-mode/ (superuser only)