NMB. · Tutti · MASTER · LIVE
Nimbus CMS
tutti, sostenuto
A lightweight PHP CMS an agent can run — and a human can read.
- Stack
- PHP · MySQL · MCP · Caddy · Docker
- Role
- Creator
- Years
- 2026
- Status
- LIVE
- Dynamic
- fff (100 of 100)
My own headless CMS: structured collections, a readable core, a scoped read+write API, and an MCP control surface so an agent can define content types and operate the site under token scopes. This portfolio runs on it — the master bus every other project routes through.
Orchestration · architecture
Nimbus CMS is a small PHP content system I built with one sentence taped to the monitor: a lightweight PHP CMS an agent can run — and a human can read. Those are two different readers with two different needs, and nearly every design decision in Nimbus is a negotiation between them. Version 0.1.0 shipped as a beta on 2026-09-04. The site you are reading runs on it.
Two readers
The agent wants a control surface: a stable, typed, machine-legible way to define content and push it around, with clear errors when it gets something wrong. The human wants the opposite of magic: plain files, a request path they can trace with a finger, and no framework between them and "why did this page render like that?"
I decided early not to serve one reader by hiding the other. The agent's API is generated from the same schema the human edits, and the human's templates are the plain PHP the agent's content lands in. Nothing is bolted on the side.
The content model is the API
Content is organised into collections. A collection is either of kind collection (many entries: posts, projects) or single (exactly one: the homepage, the about page). Each has typed fields: text, textarea, number, date, media, relation. An entry carries four reserved attributes — title, slug, status, published_at — plus a fields object holding whatever the collection defines.
Here is the move I think is actually interesting. Nimbus exposes a JSON-RPC endpoint at /api/v1/mcp, and rather than one generic "create entry" tool with a collection parameter, it generates a tool set per collection: create_projects, get_projects, update_projects, delete_projects, and the same four for every other handle. Alongside those sit schema tools (create_collection, delete_collection) and settings tools. So the schema literally becomes the API: add a collection and four new tools appear whose inputs match its fields. The agent isn't pushing rows into a bucket; it's defining content types and operating the site through them. There's a plain REST API at /api/v1 too, with the same scoped reads and writes, for clients that don't speak MCP.
A core you can read in an afternoon
There's no heavy framework. A small HTTP kernel routes the request and resolves the collection and entry. View::render renders the content template first, then wraps the result in the layout — the order you want when the content decides things the layout needs, like the page title. The theme is plain PHP templates plus assets, resolved through a short fallback chain: entry-{handle} beats entry, collection-{handle} beats collection. That's the whole templating story, and I mean that as a feature.
Read first, then write
Every update_ and delete_ call must carry the entry's current version. You read, you get a version, you send it back; if anything wrote in between, the write is refused. While seeding this portfolio I hit that refusal myself — my agent updated an entry, then tried to patch it again with the version from its first read. The API said no, correctly. Annoying for thirty seconds, then I was glad: an agent looping hard at an API is exactly the client that would clobber its own work without this check.
The CSP is a design constraint, on purpose
Nimbus ships a strict, nonce-only Content-Security-Policy: default-src 'self', scripts and styles only with a per-request nonce, object-src 'none', frame-ancestors 'none', X-Frame-Options: DENY, form-action 'self'. Theme assets carry the same policy. In practice a theme means system fonts, visuals drawn in CSS, no external assets, and every inline script tagged with the nonce. It rules out a lot of shortcuts. It also means a Nimbus site can't be quietly loaded with a third-party tag, and every theme — this site's included — is self-contained by construction. I'd rather that constraint live in the platform than in a checklist.
Running it
Nimbus ships as a Docker image. MySQL holds the content; Caddy sits in front as the reverse proxy; OPcache is on in production, so compiled templates are cached and you restart the container to pick up template changes. There's an optional page cache keyed by path, off by default. Auth is bearer tokens with scopes — schema:write, settings:write, :read, :write — minted and revoked from the bin/nimbus CLI with token:create, token:list and token:revoke.
The dogfood
Every collection and entry on danmat.dev — this page included — was created over the MCP API by an agent holding a scoped token. I didn't paste content into an admin form. If you want to know whether "a CMS an agent can run" is a real thing, you're looking at the evidence.