SvF

Writing 18 Oct 2026

18 Oct 2026 · 4 min read · EchoHex · architecture · product

The CMS I stopped rebuilding

Every new site used to get a fresh copy of my admin code, and every copy drifted. EchoHex is what happened when I made it one product instead.

By Stephan von Falkenstein

For years, every new site I built got its own admin: a login, a blog, a media library, some analytics. Each time I started from the last one, adjusted it, and moved on.

It felt efficient, and it wasn't. Every copy was a fork. A security fix on one site never reached the others. A better analytics page on one made the rest look neglected. After a while I had five slightly different CMSs, and none of them was the good one.

EchoHex is the decision to stop doing that. It's one product, with one codebase, that sits behind my sites. This site runs on it, and so does the blog you're reading.

What it is

EchoHex is a small CMS written in plain PHP with a SQLite database. There are no dependencies, no build step, and no framework, so it runs on ordinary shared hosting, and deploying it means copying files.

It ships as modules: blog, media, analytics, security, inquiries, settings, an on-page editor, payments and a self-updater, among others. A site switches on the ones it needs.

The rule I hold myself to now is simple. When a site needs a CMS, I copy the product. I never write another one.

Three ideas that made it work

A static front door

Most of my sites are static HTML at the front, with EchoHex behind it at /admin. The public pages work even with PHP switched off. Visitors load plain, fast pages, and the application only exists where it's actually needed.

Updates that can't touch your work

EchoHex updates itself from its own release channel. The obvious risk is an update overwriting something that belongs to the site, so that's enforced in code, not just written in the docs.

An update may write to the CMS's own files and nothing else. The site's pages, its app folders, its config, its uploads and its database are all off limits, and any attempt to write there is refused and logged. The relationship is the one a plugin has with WordPress: the core and the app each update themselves, and neither reaches into the other.

The fix goes back into the product

While moving this site onto the current version, I found a bug: one line in the analytics page script broke the globe and the security tab. Fixing it here alone would have been the old habit. The fix went into EchoHex, so every site gets it with the next release.

That's the whole point. A fix on one site becomes a fix for all of them.

What it taught me

A test that doesn't match production can pass and prove nothing. The first time I updated a live site, the update moved the file that opens the database. The CMS looked in the new place, found nothing, and created a fresh, empty database. The site was healthy and empty, and it reported success the whole time.

Nothing was lost: the data was still on disk, and I moved it back. But my test install had passed perfectly, because it was laid out differently from the real site in exactly the way that hid the problem. Now every site pins where its database lives, the updater refuses to create a blank one when real data exists elsewhere, and I rehearse every install on a copy shaped like the live site.

Count the rows, don't trust the message. "Update applied" isn't a result. The same number of rows before and after is.

A product needs one source of truth. Ten good-enough copies are worth less than one maintained one.

EchoHex isn't for sale yet. For now it's the quiet part of everything I ship, and I'd rather it stays boring and dependable.