Dan Matthew

Writing · 6 Sep 2026

How a portfolio CMS turned into a platform when I stopped guessing

NimbusCMS started as a portfolio project, a small PHP CMS an agent could run and a human could read. The turning point was the day I made a rule that stopped me adding features to it.

#nimbuscms #php #cms #platform

NimbusCMS started as a portfolio project. I wanted a small PHP CMS that an AI agent could operate and a human could still read, and I wanted to build the whole thing in the open rather than talk about building it. What I did not expect was the point, a couple of months in, where it stopped being a CMS I added features to and became a platform that told me which features to add. This is the story of how it got from there to here, including the part I got wrong.

It began as Panelix

The first name was Panelix. I renamed it to NimbusCMS on the nineteenth of July 2026, one gh repo rename and a lot of find and replace. Nimbus, as in a cloud you fly on, which gave the whole thing a gentle broom and night sky theme: a gold broom for a logo, an indigo starfield down the side of the admin, a little spell flavored microcopy. Trademark safe, just a vibe.

The early decisions were the boring, load bearing kind. Its own schema and a Collections model, so content types are defined in the UI and entries are stored as typed fields, not welded to a fixed blog shape. Argon2id passwords. A forward only migrator. A plain HTTP kernel you can follow with your finger. No heavy framework between you and the question "why did this page render like that". The order of the first features was written down and followed: a media library, then a headless JSON API with tokens, then editing, then roles.

The wedge I did not plan for

Somewhere in the API work, the project found its actual identity. I was building the token system so a script could operate the site: scoped tokens, permissions checked deny by default at the query layer, strong ETags so a writer cannot clobber a concurrent edit, a generated OpenAPI document. And I realised I was not building a machine API bolted onto a human CMS. I was building for a second reader who was every bit as real as the first.

So I leaned all the way in. The content model became the API: define a collection and Nimbus generates a typed tool set for it, one the agent drives over MCP at a single JSON-RPC endpoint, gated by the exact same permissions a person has. An agent can define a content type, create entries, manage media and users and tokens, all of it scope checked, non enumerating, and audited. Older CMSes are busy bolting an agent interface onto a human first architecture. Nimbus was built for the agent as a first class operator from the token layer up. That is the wedge, and it was not on the original list. The work led me there.

By late summer it had a real spine: the plugin system with a handful of capabilities, official plugins each in their own repo with their own CI, a public themeable site with blocks and sitemaps and a page cache, and a roles system where a capability bundle grants both people and tokens their authority through one shared rule.

The rule that changed everything

Here is the part worth the blog post. On the third of August I stopped adding features.

Not literally, but I changed what was allowed to justify a feature. Until then, the honest driver of "should this go in the core" was my imagination. I would think of something a site might want and build it. The trouble with a core is that you cannot take it back later, so every imagined requirement I guessed wrong became permanent weight.

So I wrote a charter and gave it the final say over me. From that day, a capability lands in the core only if it is broadly reusable across unrelated sites and it passes a three perspective review, product and architecture and engineering, each on its own terms. Anything shaped like one specific site stays out. And to keep myself honest I added one question I have to answer before anything enters core: would I still build this if my one test app did not exist. If the answer is no, it is a plugin, not core.

Then I built the test app, on purpose, to break things.

Proving it by building on it

I took my oldest repo, a 2014 restaurant management system that would not even start on a modern PHP, and rebuilt it as an application running on Nimbus. The goal was never the restaurant. It was to see whether a whole real system, floor and orders and kitchen and payments and staff roles and reservations and reports, could be built with zero restaurant logic in the CMS core. Where it could not, the gaps would be the honest to do list.

They were. The restaurant pulled exactly four small, general things into core, each with its own decision record: a read only way for a plugin to read published content it does not own, finer grained plugin permissions than a plain read or write, a per site allowlist for embedding, and a plain dashboard bug it happened to expose. Every one of those is useful to a shop or a docs site, not just a restaurant. The restaurant specific parts, the tables and the tickets and the reservations, stayed in the plugin, on the plugin's own storage, touching no core data. I wrote a longer post on that pattern, because it is the most useful idea in the whole project.

Where it is now

Nimbus shipped as a 0.1.0 beta at the start of September 2026. There is a core, an official plugin ecosystem growing around it (a CRM, an inventory ledger, the blog you are reading this on), and real applications built on top: the restaurant, and a grocery store next. My portfolio at danmat.dev runs on it. So does the marketing site. This very page is a content entry in a collection, rendered by a theme, with the blog specific parts living in a plugin.

What it is not yet

It is a beta, and I am deliberately slow to freeze the public surface, because a public core capability is hard to walk back. One test app is evidence, not proof, so I treat a capability as broadly wanted only when I can name the unrelated sites that would use it, not when I can merely picture them. The grocery store is the second opinion. The honest summary is that the interesting part of this project was never a feature I added. It was the day I took away my own permission to guess.