The document mirror moved offices: work items to Plane Pages
The document mirror moved off the kanban board and into Plane Pages, and the storage contract forced a full identity redesign.
TL;DR
The mirror moved from Work Items to Pages, which preserves HTML comments instead of sanitizing them, forcing a new identity design. Pages are flat with no folders, need full replacement on update, and require archiving before delete with verify-after-write. Sync now trusts content hashes, allows two-way edits only for meeting notes, and keeps library docs one-way with divergence alerts.
I opened the new spec diff in my terminal and remembered the old arrangement: the previous generation of the document mirror forced every file onto a kanban board as an ordinary work item. Documents sat in status columns with assignees attached, like chores. That was always odd. A document is not work to be dragged to Done; it wants to be read and edited as a document.
My first guess was optimistic: point the sync engine at a pages endpoint, swap the payload, done. Wrong on a fundamental level. What changed was not the endpoint address but the storage contract, and the contract is what forced me to tear the mirror's identity design apart and rebuild it.
The contract that flipped direction
I would phrase the core lesson like this: a sync mirror is only as trustworthy as the least-mutating endpoint it talks to. So probe the storage first, then let the storage shape the identity design, never the other way around.
In the old version, work items sanitize HTML server-side through the nh3 allowlist. HTML comments are stripped outright and multi-element fragments get re-wrapped. That is why the v2 sync marker had to be a plain first line of text: the only form guaranteed to survive was visible text. When I probed Pages on the Community Edition, every one of those assumptions inverted: HTML round-trips verbatim, HTML comments included. The v3 marker finally gets to hide inside a leading comment without being eaten by a sanitizer.
The create/update rules are equally specific. Creating a page requires description_html in the body [1]. Updating replaces the entire page content with what you send; locked or archived pages refuse updates, and the collaborative document service behind the editor can answer 502 or 503 [2]. Had my engine still assumed append semantics, a single push could have wiped half a document without a single error message.
Hierarchy, it turns out, is an illusion here. The parent_id parameter is ignored on create, so pages live in one flat namespace. A naming convention replaces the folder tree: meeting notes use MEETING: date - title, library documents use DOC: relative-path. The list endpoint returns an envelope without bodies, so a full status check needs one retrieve per page. I accepted that trade-off, because the drift gate only needs the updated_at column from the envelope and never touches bodies at all.
The page lifecycle has its own traps. Deleting an active page answers 400; the page must be archived first [4]. Restore, absurdly, is a DELETE call against the /archive/ sub-path, and parents must be restored before their children [5]. My own probe added one fact the docs do not mention: a fully deleted page answers a retrieve with 403, so the recreate flow treats that 403 as the missing-page signal rather than crashing. And the is_locked flag blocks every update; the engine reports it as blocked instead of forcing its way through.
The official documentation, meanwhile, is more conservative than the server I probed. The content contract warns that Plane sanitizes the HTML, recommends fetching the page after a write if your integration needs to inspect the final stored HTML, and caps the payload at 10 MB [3]. My live probe came back byte-identical. Documents act as the conservative ceiling, the probe as today's reality; both went into the design, with verify-after-write as the safety net.
One small detail reinforces the same pattern: images. Pages on the Community Edition have no attachment upload route at all, and binaries stay in git. Instead of faking an upload, an image reference like  in the markdown renders as a visible placeholder pointing at the file path in the repo, and the pull side maps it back. Alt text is even canonicalized away so the markdown-to-HTML-and-back trip stays byte-identical. The message is clear: if the target system cannot store something, do not pretend it can; point the reader at the source of truth.
Two tiers, hash as the authority
The region split is simple: meeting notes sync two-way so they can be edited from a phone, every other library document is one-way publish from git, and pull is refused by design with an explanatory message. Git remains the only editor for the library; if someone edits a page from the Plane side, the status lights up as a divergence alarm with both mismatching hashes on display.
Authority lives in the hash: a sha256 of the normalized markdown body is computed on both sides and it alone decides status. Page names and timestamps are diagnostics. A marker whose path does not match the configuration is reported as blocked, and a single reconcile door in plane-init upgrades a v2 config to v3 without ever silently repairing mappings.
My favorite part is the property test: for every fixture and every file in the documents tree, markdown to HTML and back to markdown must equal the normalized markdown. That invariant is what makes a deterministic converter trustworthy, and if the conversion subset ever grows, this test is what screams first, before the mirror gets a chance to corrupt anyone's documents.