Plane Is Live, and It Lied About Being Healthy for Ten Minutes
Project tracking joins the stack — and an agent now files work items into it over the API. Getting there meant learning that a 200 response from Plane means the proxy is alive, not that Plane works.
Why Care?
The stack could always hold your projects. It could not track the work of building them. Plane closes that — the Linear/Jira lane, self-hosted, running for us first.
But the reason it earned a slot in core/ is narrower than "we needed a tracker."
It passed one specific test that has disqualified better-looking products:
the REST API, webhooks and personal access tokens are free in the community
edition. That is exactly where Docmost lost the knowledge-management slot — its
API keys sat behind an Enterprise licence, and agent access is a headline feature
of every stack we ship.
Proof rather than assertion: within an hour of deploying, an agent minted a token and filed eight work items over the API. The backlog in there was written by something that had just spent the evening finding the bugs it was reporting.
What's New?
Plane deployed for
lossless— five Railway services, migrations clean, admin account live.core/planepinned at v1.4.1 (upstreammakeplane/plane, not forked).docs/plane/setup.md— a runbook that now describes a deployment that happened, with five gotchas that cost real time.A new README category, Project & Issue Tracking, with the honest caveat that Plane's community edition has no SSO.
Agent write access — token and instance URL in
~/.secrets, declared insecretspec.toml.An issue on Postiz's footprint, because measuring the fleet turned up something uncomfortable.
The bet that made it affordable
Plane's standard self-host topology is thirteen services: web, admin, space, api, worker, beat-worker, live, migrator, proxy, plus Postgres, Redis, RabbitMQ and MinIO. On Railway that is roughly ten services per client — by far the heaviest thing in the stack, and honestly enough to make Plane not worth it.
Plane also ships an all-in-one image that runs all nine app processes under supervisord in one container. That leaves four external dependencies and collapses Plane to a Twenty-shaped footprint:
plane makeplane/plane-aio-community:v1.4.1 9 processes, supervisord
plane-postgres postgres:15.7-alpine volume
plane-redis valkey/valkey:7.2.11-alpine
plane-mq rabbitmq:3.13.6-management-alpine volume
plane-storage Railway bucket replaces MinIORabbitMQ is the one genuinely new dependency Plane adds — AMQP_URL is required
and the app will not boot without it. Datastore pins follow Plane's own compose
rather than our house defaults: Postgres 15, not 16, and Valkey, not Redis.
A 200 that meant nothing
This is the part worth carrying forward.
After the first deploy, everything said healthy. /healthz returned 200. The
Railway healthcheck passed. The deployment reported SUCCESS. A poller printed
UP. Every automated signal agreed.
The page said:
🚧 Looks like Plane didn't start up correctly!
Plane's Caddy proxy answers 200 OK while serving that. So the proxy being
alive and the application working are different facts, and only one of them is
observable by curl.
It was caught because step 9 of the runbook says to load the page in a browser. Without that habit it would have shipped, and the failure would have surfaced as a confused human staring at a spinner.
A green pipeline is not evidence. Look at the thing.
What we broke, and how
The underlying failure was ours.
Migrations were running on first boot when a variable got changed. That triggered a redeploy which killed the migrator mid-transaction — and, worse, left two containers running migrations against the same database at once. Two Django migrators racing is what produced this:
ERROR: duplicate key value violates unique constraint "pg_type_typname_nsp_index"
ERROR: column "sort_order" of relation "stickies" already exists
ERROR: relation "global_views" does not existDjango never records an interrupted migration, so it retried forever against a schema that already had the objects. Supervisord restarted the migrator every ten seconds, each pass making it worse.
Recovery, safe only because no data existed yet: drop and recreate the database.
Two mechanics cost a cycle each — DROP DATABASE cannot run inside a
transaction block, so psql -c "DROP …; CREATE …;" fails and they must be
separate calls; and WITH (FORCE) is what evicts the crash-looping connections
that otherwise block the drop.
Then a second, quieter failure: two deployments failed their healthcheck at
exactly 600 seconds because Railway probes $PORT while Plane's Caddy binds
:80. Railway rolled back to the container from before the database wipe —
which is what the operator was clicking on while everything looked fine. PORT=80
fixed it. Same shape as Twenty's PORT/NODE_PORT trap, which cost a failed
deploy the first time too.
The setup page needs a trailing slash
Worth stating plainly because it will recur on every AIO deployment.
Plane's own "Get started" button links to /god-mode. The admin app's router
is mounted at /god-mode/. The bare path serves HTML that renders nothing, and
the console says exactly why:
<Router basename="/god-mode/"> is not able to match the URL "/god-mode"
because it does not start with the basename, so the <Router> won't render anything.Compounding it, the admin shell ships no cache-control header — only an
ETag and a last-modified weeks in the past — so a browser that loaded the page
while it was broken will happily keep serving that copy.
Go to /god-mode/ directly, in a private window. Don't use the button.
What measuring the fleet turned up
Checking whether Plane was affordable meant measuring everything, and the answer reframed how we think about cost:
Memory is 93% of the Railway bill. $48.56 of $52.40 last month. CPU was
$2.39. Egress was nine cents. twenty-server averages 0.0004 vCPU and costs the
same as if it were busy.
So cost tracks resident memory of always-on containers, not users. Which makes the per-seat pitch on the landing page structurally true rather than marketing.
It also surfaced this:
| the-water-foundation | memory |
| postiz | 2,648 MB |
| temporal + temporal-postgres | 465 MB |
| postiz datastores | 56 MB |
| Postiz and dependencies | 3,169 MB — 56% of the project |
That instance of Postiz has never been usable — its FRONTEND_URL sits on
*.up.railway.app, a Public Suffix, so browsers refuse its auth cookie. The
largest line item in that project is a tool nobody can open. Filed as an issue
rather than acted on; whether the capability is wanted is not an engineering call.
The tracker is already tracking itself
Eight work items now sit in Plane, filed over the API, carrying evidence rather than titles — the memory numbers, the cookie-domain explanation, the exact command to run:
SELF-1 high Confirm Plane sends invitation email
SELF-2 high TWF Postiz is deployed but cannot be logged into
SELF-3 high Prove SMTP with one live send across all four Twenty instances
SELF-4 high Pull dangling didi.sh DNS records for deleted hub services
SELF-5 medium Reconcile what is deployed against what clients.ts routes
SELF-6 medium Add Plane to clients.ts now that it is reachable
SELF-7 medium Verify pricing on the self-host-stack catalog
SELF-8 low Delete orphaned temporal-postgres in reach-eduDeliberately not done
No
clients.tsentry yet. That file is the portal's routing table — what a client can open — not an inventory of what runs. TWF's Postiz is the cautionary tale: deployed, healthy, unusable, unrouted, unnoticed for eleven days. Plane gets its route in a separate, deliberate change.Email unproven. Plane has never sent a message. Given Twenty's
LOGGERdefault stranded a stakeholder for four days across four instances, assume it is broken until an invitation lands in a real inbox. That is SELF-1.Backups not wired. The
pg-dump-planeservice does not exist yet.
