
Five borrowed ideas, and where they collide
Nothing in the design is mine. The interesting work is in the seams, because the ideas contradict each other and the contradictions are not obvious until you write them down together.
The borrowings, with attribution, since none of this is original:
Flat files as the source of truth, from Kirby and Statamic. A folder per document holding its Markdown and its images, for the reasons I wrote about in May last year.
The body as structured blocks, from Sanity and Ghost. An ordered array of typed blocks rather than an HTML blob.
The model as code, from Payload. Collection definitions as files, reviewed and deployed.
Hooks for extension, from WordPress. Named points where a plugin registers a callback.
Hot reload and plugins as ordinary code, from Neovim and LÖVE.
Now the collisions.
Files versus blocks. Kirby says the Markdown file is the truth. Sanity says the body is a block array. These are not compatible. A block array cannot round trip through Markdown, because Markdown has no way to express a gallery, so the moment you edit through a block editor and save, either the file stops being the truth or the gallery is lost.
The resolution I have landed on is that blocks Markdown can express stay Markdown, and blocks it cannot go into typed fenced regions:
```lagom:gallery
{ "images": ["one.jpg", "two.jpg"], "columns": 3 }
```
The fence is the storage, so there is nothing to round trip. The file stays readable and the API still serves an array of typed blocks. This is close to what Kirby does with KirbyText tags, arrived at from the other direction.
Files versus concurrent writes. Files have no transactions. The admin writes, a person writes in vim, git pull writes. Last write wins and the loser gets no error. Every write therefore has to carry the hash of the file it was based on, and be rejected if the file has moved on. Making that optional would make lost updates the default.
Hooks versus isolation. WordPress plugins share a process and can reach anything. Neovim's do too and are honest about it. The temptation is to run plugins in a separate Lua state for real isolation, and it does not survive contact with the requirement: a separate state has no access to the request API, no asynchronous I/O, and every call across the boundary needs serialising. That is most of what makes a plugin useful. So plugins are trusted, run in the main state with a restricted environment, and the documentation says plainly that this stops accidents rather than attackers.
Hot reload versus multiple workers. Reload is per process. With four nginx workers, reloading in one leaves three serving the old code, and requests alternate between them. Neovim does not have this problem because there is one of it. So reload is a development affordance, refused unless there is a single worker, and production reloads nginx instead, which is already zero downtime and boots in under a hundred milliseconds.
An index versus multiple workers, which is the same shape again. An in memory index is per process, so four workers means four divergent indexes and four file watchers racing to write them. One worker watches and writes. The rest read a shared SQLite file in WAL mode.
Five ideas, four of which needed a resolution before any code was worth writing. That is the argument for writing a specification first, and the specification is mostly a list of these collisions with a decision attached to each.