// history

The filesystem, the shell and the template language are older than the platform.

Three ideas from a CMS released in 2006, a rewrite that never shipped, and what each of them cost. The dates are checkable, and the code below is quoted from the archives rather than described.

2000 · 2003 · 2006 · 2011 · 2016 · 2026

// 2000 — 2001

It started in a newsroom

In 2000 the job was to put a newspaper online. There was not much material at first, then there was more, and the process had to be automated. The first CMS followed in 2001. There were earlier attempts in Perl; they are gone.

The question a newsroom asks every day

// which is why this has owners and modes, not a role matrix
who

Who may change what

Several journalists, editors above them, sections, a piece one person writes, another edits and a third publishes. There, that is not an abstract question. It is the daily one.

mode

An owner, a group and a mode answer it

A checkbox labelled "can edit articles" does not. That decision was made once and has never been revisited.

// 2003 — 2006

CoolMS.CMS, named in 2003, released in 2006.

Procedural PHP, no framework: 24,235 lines across 52 files of source. Three things in it are still here.

Three ideas that outlived the code they were written in

// still here, twenty years later
vfs

A virtual filesystem with real unix semantics

Not a metaphor, and not a permission model bolted on beside one — a mode, an owner, a group and three timestamps on the row itself, with a journal table for versions. Today's VFS is the same model with twenty years of consequences worked out.

sh

A shell over it

Twenty-eight commands with the names you would expect. Today's Terminal module is not a new idea; it is the same idea returning.

dtmpl

A template language

Braces, colons, backtick-quoted strings, loop and item, split and reverse. Today's DTMPL is the same language with escaping on by default and a fixed allow-list where there used to be none.

The 2006 table, quoted from the archive — coolms.cms, src/db/mysql/vfs.sql. A second table, vfs_journal, holds the versions.

column type default

mode SMALLINT (3) UNSIGNED DEFAULT 644 NOT NULL
uid INT (11) UNSIGNED DEFAULT 0 NOT NULL
gid INT (11) UNSIGNED DEFAULT 0 NOT NULL
ctime INT (11) UNSIGNED created
mtime INT (11) UNSIGNED modified
atime INT (11) UNSIGNED accessed

// coolms.cms, src/cmd.phl

The twenty-eight commands, in full

  • cd
  • chmod
  • chown
  • copy
  • cp
  • del
  • erase
  • exec
  • format
  • groupadd
  • groupdel
  • groupmod
  • index
  • mkdir
  • move
  • mv
  • regadd
  • regdel
  • regmod
  • restore
  • rm
  • sendmail
  • system
  • touch
  • update
  • useradd
  • userdel
  • usermod

// what useradd did in 2006

A group named after the user, and a home directory at mode 700

It took --expire, --home and --groups, created the group if it did not exist, and made the home directory owned by the new user with mode 700 — the same arguments and the same default as the tool it is named after.

The template language, quoted from the archive — coolms.cms, src/classes/dtmpl.class.phl, whose header reads "@copyright Copyright (c) 2003-present" and "@version 0.4.19".

const value

OPEN_BRACE {
CLOSE_BRACE }
COLON :
QUOTE `
EQS =

// the front end, before jQuery

Four engines that agreed on very little

The front end was hand-written JavaScript, because jQuery was barely out. It worked identically in Internet Explorer, Netscape Navigator, Opera and Safari. Nothing in that era was portable by default; it was portable because someone tested it in all four. From 2006 the platform ran the work of a web studio, Altgraphic, built around it.

// 2011 — 2016

The rewrite that never shipped

From 2011 the platform was rebuilt on Zend Framework — started on ZF1, finished on ZF3. Doctrine ORM replaced the hand-written storage layer, Dojo Toolkit replaced the hand-written JavaScript, and around twenty-nine modules were published to Packagist as separate Composer packages. Much of it was ahead of its time and stayed right.

Three bets from the rewrite that the industry made later

// ahead of its time, and it stayed right
evt

Event sourcing, as a dependency

coolms/common required prooph/event-sourcing and prooph/event-store-bus-bridge — not as an option, as a dependency every module inherited. The panel below is that package's own published requirements.

ui

Modules and classes before the language had either

Dojo Toolkit had both years early and was eventually rewritten in TypeScript. Picking it in 2011 was picking the shape the industry arrived at later.

slots

A UI module changing the document

coolms/layout did multisite, sections, and let a UI module change the tags in the document through configuration rather than by editing a template. That is what this platform's slots, fills and gathered assets do today.

coolms/common, last released 9 February 2016 — from packagist.org/packages/coolms/common, where it now stands abandoned.

require

require prooph/event-sourcing ~4.0
require prooph/event-store-bus-bridge ~2.0
require zendframework/zendframework >=2.5.0
require php >=5.5.0

// and one thing was wrong

The storage adapter became the integration point for the whole platform

Nobody decided this. The domain package was clean. But Doctrine's mapping lives on the entity, so an association has to be declared there — which meant every module wanting to attach something to a user had exactly one place to put it. The ORM user entity is 894 lines. It imports from fourteen modules and mixes in nine traits: shopping cart, wallet, files, organisations, roles, locale, extended metadata, login tracking.

coolms/user, released 2 December 2015 — from packagist.org/packages/coolms/user. Three sibling domain modules required; Doctrine only suggested.

require / suggest

require coolms/authentication dev-master@dev
require coolms/common dev-master@dev
require coolms/mailer dev-master@dev
suggest coolms/doctrine-orm for using Doctrine 2 ORM

// it never reached a release

The ORM had become the centre of a platform designed to keep it at the edge

And that was visible before anyone was running it, which is the difference between this and a post-mortem: what it cost was years of work, not a decade of maintenance.

// for the record

Two bets on a version number that never arrived

The earliest attempts were on Perl, betting on Perl 6. The rewrite was on PHP, betting on PHP 6. Neither ever shipped. 🙂

// 2016

Two branches, one person.

The 0.x line was still being changed into 2014, the 1.x packages were still being published into 2016, and neither could be finished while the other was alive. It ended the way that ends. What followed was a decade of employment on other people's large systems — enough of them to learn that the practices which survive are the ones a machine enforces, and the ones that live in a style guide do not. None of that is in the 2011 code. Most of it is in this one.

// 2026

The ideas needed a team and an investment that were never going to appear

What changed is that work which used to require a team became possible for one person. The decision was made at the start of 2026: finish it now, or accept that it never gets finished.

This platform was begun in February 2026.

// it is built the way it is described

Two sessions, a shared ledger, and each re-measuring the other

They work in parallel on separate parts of the tree and re-measure each other's claims — most days each finds several errors in the other's work that neither would have found alone.

What a machine checks here, rather than a person

// a rule enforced by attention is not enforced
rules

The architecture rules

The boundary between a module's domain and its persistence is checked by static analysis rather than by convention — the rule lives in a tool that evaluates it, not in a document that asks you to remember it.

clone

The install path

Verified by a workflow that runs the documented steps against a clean clone, because a page describing an install nobody has walked is fiction.

orm

The storage boundary

PHP 8.5, Symfony 8, and Doctrine as an adapter behind a boundary the compiler checks — the exact thing the 2011 rewrite intended and could not hold.

// coolms/core, today

Declares a persisted column without naming a persistence library

That is the first line of the docblock on Column in coolms/core, verbatim. It goes on: "The reusable traits carry these instead of an ORM's own mapping attributes, so this package depends on no persistence library at all. A mapping driver inside the persistence adapter translates them." That is what 2013 intended and what 2026 enforces.

// published again

Twenty-nine packages went to Packagist in 2015. They are going again now.

The difference is not ambition — it is that this time the boundaries are checked by something other than the person who drew them.

// what survived

The filesystem still has owners, groups and modes.
The shell still has chmod.

The template language still uses braces and backticks. Twenty-six years, three ideas, and one difference that matters.