A design is not finished when there is nothing left to add. It is finished when the open questions have been closed, including the ones you would rather leave open.

What got locked.

Runtime. OpenResty with no framework. Lapis is the obvious choice and I am not using it, because what it offers is a Postgres ORM, migrations and etlua templating, and this design uses none of the three. A router over path segments is a couple of hundred lines.

Storage. Markdown with YAML front matter, a folder per document, media beside the document that owns it.

Index. SQLite through the FFI, WAL mode, treated as disposable. Two databases, not one: the index is derived and rebuildable, while users, sessions and tokens exist nowhere else and need durability. Putting sessions in the derived database would log everyone out on reindex.

Writes. Hash guarded, atomic through a temporary file and a rename, with deletes going to a trash directory instead of unlinking.

Auth. Split by consumer. The admin gets an httpOnly SameSite cookie so no credential reaches JavaScript. Machines get scoped bearer tokens. One mechanism for both would compromise one of them.

API. REST only. GraphQL is not deferred so much as declined: there is no mature GraphQL server in Lua, so it would mean writing schema generation, query parsing and batching from scratch, and I have not met the person who needs it here.

Admin. A separate application built in CI and served as static files, talking to the same API as any other client. No private endpoints.

Licence. AGPL. Network copyleft, because the failure mode for a self hosted CMS is somebody running a modified copy as a service and contributing nothing.

What got rejected, which is the more useful list.

No theme system. This is the WordPress model and it is the coupling I wrote about in November 2024. Instead there is a reference frontend, in Astro, that reads the API and is meant to be forked. That is the theme story done the way a headless system should do it, and the core stays frontend agnostic.

No GraphQL, no visual page builder, no multi tenancy, no plugin marketplace.

No keyword scoring in whatever SEO tooling eventually shows up, though that one is getting ahead of myself.

The rule I am using to decide is the name. Lagom means the right amount. When a feature can be argued either way, it stays out, and the argument for adding it later is always available while the argument for removing it later never is.

The specification is about four thousand words and every decision has the reasoning attached rather than just the conclusion. That is deliberate. In six months I will not remember why the index is a separate database, and a document that records only the decision would leave me free to undo it for a reason I already considered and rejected.

The next one is written after building it, and the honest summary is that the specification was wrong in about a dozen places.