The tinycycle
Every node — every piece of graph data — has its own lifecycle: it is drafted, approved, revised, retired. In CTXR that node-scoped lifecycle has a name we coin for it, the tinycycle. “Tiny” is the point: it has very few states and very few transitions, on purpose. Workflow states — “scheduled”, “in review”, “archived” — create complexity that most sites don’t need and every site pays for. CTXR leaves them out.
Each node runs its own tinycycle independently. A node sitting in draft doesn’t hold back a sibling that’s already live; approval happens one node at a time. And because approving a node is exactly the moment its references are resolved, a node’s tinycycle is what drives changes into the compiled graph — which is why this term builds directly on the one before it.
Two states that matter
A node is draft-only, live, or both at once:
| State | What exists | Visitors see | Editable |
|---|---|---|---|
| Draft only | A draft | Nothing | Yes |
| Live | A live record | The live record | Make a draft first |
| Live + draft | Both | The live record; the draft is hidden | The draft |
That’s the whole model. While an editor works on a draft, the live version is completely untouched — visitors keep seeing published content right up until the moment of approval.
The transitions
create → a draft exists
save → a draft property changes
approve → the draft is compiled into the live graph
discard → the draft is dropped, live is unchanged
unpublish → the live record is removed, reverts to draft-only
delete → everything is removed
Approve is the one that does the heavy lifting. It’s the moment the compiler runs: the draft becomes the source, every reference is resolved, derived values are calculated, the result is written as the live record, the draft is cleared, and the change cascades to everything that referenced this node. One click; the platform handles the rest.
The others are intentionally cheap. Save touches only the draft. Discard reverts a mistake in milliseconds without rebuilding anything. Unpublish removes a node from the public site but keeps it as a draft, cascading so that pages which included it are rebuilt without it. Delete removes everything; references to a deleted node simply resolve to empty, which the compiler handles gracefully.
One edit, everywhere it appears
Put the tinycycle and the compiled graph together and a useful guarantee falls out. Because a node is inlined into every place it’s referenced, and because approving that node recompiles each of those places, a single edit propagates to all of them at once. Fix a place’s name and approve, and every route, every overview, every card that embedded it updates in the same motion.
You never hunt down the copies, because there is one source and the copies are derived. Edit once; the graph stays in tandem — no page left quietly showing the old value.
Viewing is not the same as publishing
There are two different things an editor does, and keeping them separate keeps the mental model clean.
Approving changes state: a draft becomes live. It’s an action with consequences — the compile, the cascade, the public site updating.
Viewing as changes nothing. Adding ?live or ?draft to a URL just selects which version of the same node you’re looking at. It’s a lens, not a lever. “Live” and “draft” aren’t two kinds of page; they’re two projections of one node, which is why switching between them is instant and harmless.
Why this matters for templates
Because there’s one node behind both lenses, there’s one template behind both audiences. Templates don’t have separate read and edit modes.
For visitors, a template receives the compiled live graph — pre-resolved, complete, ready to render. For editors, the same template receives the draft merged with live data, plus a few runtime flags: _isEditor (this viewer may edit), _hasDraft (an unpublished draft exists), and _diff (which properties differ between draft and live). The editing affordances — the highlight on a changed title, the inline controls — activate from those flags, not from a different URL. A template can read the diff directly to drive its own indicators:
$diff = $data['_diff'] ?? [];
$nameChanged = array_key_exists('name', $diff); // true when the draft title differs from live
These keys describe the current request only; they’re never stored on the node (see nodes & types).
So a page is written once and serves everyone. The editor experience isn’t a separate application bolted on; it’s the same page, reading the same data, told who’s looking.