Skip to content

remill

Why I built remill

Slugwhy-i-built-remill
ExcerptIn 2018 I wrote a small CMS with one good idea and three bad security holes. remill is that idea rebuilt for a world where the client might be an AI agent — with the holes made structurally impossible.

Every developer has one old project they can't stop thinking about. Mine was called Blogmill.

I wrote it around 2018 — a small Node, Express, and MySQL CMS to run a few websites. By the standards of the code I write now it was a mess: process globals, jQuery and Grunt, a TinyMCE box that spat HTML blobs into the database, and SQL assembled by gluing strings together. None of that code survives in remill, and none of it should. But buried in the middle of it was one idea I've never been able to improve on, and I built remill to give that idea a better home.

The one good idea

In Blogmill, a content type was a single JavaScript object. You described your fields once — their names, their types, whether they were required — and that one description did four jobs at once. It generated the database columns. It rendered the list view in the admin. It rendered the edit form. And it drove the validation-and-save pipeline. The entire admin ran on two generic routes; adding a new content type meant adding one file. The engine that did all of this was about 350 lines.

I didn't realise at the time how good a bet that was. This was before Payload and Directus made "your schema is a config object" a mainstream idea. It just felt obviously right: say a thing once, and let the machine derive everything downstream from it. Every time I've reached for a heavier CMS since, I've missed it.

The three holes

The idea was good. The security was not. Blogmill had three holes, and they're worth naming precisely, because remill is in many ways a 15,000-line argument for why each one should be impossible rather than merely avoided.

The first was mass assignment. When you saved a record, Blogmill took the entire request body and wrote it onto the row. Any field an attacker chose to include got written — including ones your form never showed. This is the classic CMS foot-gun, and it was wide open.

The second was unescaped SQL identifiers. Because content types were dynamic, table and column names were sometimes interpolated straight into SQL strings from data. Parameterised values protect you from a lot; they don't protect you when the identifier itself is built from user input.

The third was a single shared signing secret. There was one weak key, and everything trusted it. No notion of separate identities, no per-client credentials, no way to revoke one actor without breaking all of them.

Individually, each is a familiar bug. Together they describe a system where the boundary between "data the user sent" and "instructions the system trusts" was never really drawn. I could have patched all three in an afternoon. What I couldn't do was make them stay fixed as the code grew — and a fix you have to remember is a fix that eventually lapses.

What changed: the client might be a machine

I could have left Blogmill in the ground. What pulled the idea back out was a new kind of client.

For its whole life, a CMS has assumed the thing on the other end is a person with a browser. But increasingly the thing on the other end is an AI agent — something that wants to read your content, create some, publish it, and occasionally invent a whole new content type to hold data you didn't anticipate. Most systems bolt agents on at the end: a chat box wired to a few endpoints, an afterthought behind the "real" product.

Blogmill's old idea suddenly looked like the answer to a question I hadn't been asking. If a content type is just data — a description an engine reads at runtime — then an agent can define one over the wire and immediately start filling it, with no deploy, no migration, no human in the loop to write a model file. The declarative schema I'd built for my own convenience in 2018 was, it turned out, exactly the shape you'd want if your users could be programs.

That's the line I care about: agent-native, not agent-bolted-on. Not a CMS with an AI feature stapled to the side, but one where an autonomous agent is a first-class client of the same machinery a human uses.

Keep the idea, kill the substrate

So the decision was easy, if a little ruthless: new repository, new code, port only the concept. Blogmill's foundation — globals, Express, MySQL, jQuery — is simply incompatible with where I wanted to run this, and its security posture was disqualifying on its own. There was no clean incremental path from the old thing to the new one. Better to keep the one good idea and throw everything else away.

remill runs on Cloudflare Workers. It's a single Worker, one SQLite database, one object-storage bucket. It's deliberately single-tenant and deliberately small — lightweight is the product, not a constraint I'm apologising for. The admin is server-rendered with no React and no client framework. Content is exposed three ways from the same definitions: a styled admin, a JSON REST API, and an MCP server where agents are first-class. There's a fourth way now too — public rendered pages — but those three doors are the heart of it.

And the three holes? They're closed by construction, not by vigilance. Every write — from the admin, from the API, from an agent over MCP — goes through one pipeline that validates against the declared fields and rejects anything undeclared. That's mass assignment made impossible: there's no code path that writes a field you didn't define. Identifiers are never interpolated from input. And there's no shared secret — every actor, human or machine, is a separate identity with its own credentials, its own permissions, and its own audit trail. One pipeline, three doors, no exceptions. I'll go deeper on how each of those works in the next two pieces.

The proof is the artifact

Here's the part I find quietly satisfying. This article you're reading is a row in a collection called articles. I didn't create that collection by writing a migration or clicking through a form. An AI agent defined it — over MCP, in a single call — and the moment it did, the storage, the validation, the admin's list and edit views, the REST endpoints, and the agent's own tools for managing articles all came into existence together. Then the same agent wrote this text and handed it to me to publish.

That's the whole thesis, running in production, on day one: the good idea from a scrappy 2018 CMS, rebuilt so that the holes are gone and the client can be a machine. Blogmill finally grew up.