May 2026
The editor is built from the blocks it edits
Adding a database backend to a block-based editor introduced no new model tools because the apps and the editor are one tree of typed blocks, and the same purity later cost it its founding CRDT
Two designs reached the FE and BE review for putting a database behind the editor. Version A generated a SQL sidecar for every table a user dropped into their app: add a table, emit the Postgres DDL, wire a query path beside it. The reviewers killed it in a sentence. A sidecar forces every table addition to maintain a second, hand-written copy of a schema the document already holds, so it fights the structure of the system at every table the user adds.
That structure has a name worth being precise about. An app here is a JSON tree of typed Block nodes living in a single CRDT, rendered by walking that tree against a component registry. The editor’s own toolbars, inspectors, and panels are blocks in the same tree. The thing that builds apps is assembled from the same nodes as the apps it builds, which is exactly why a SQL generator bolted to the side reads as a foreign organ.
The winning design, Principle 8 in the review, materializes Postgres only from schema declarations already present in the doc. It introduced zero new LLM tools.
That clause should stop you. A database is the textbook case of a capability that drags new primitives behind it: a connection, a migration step, a query interface, some verb the model can call to define a table. Here the number of new tools the model could call was zero. The 11 tools already in the editor express schemas, relations, and triggers, because a table in a user’s app was already a block carrying a schema declaration long before any database existed. The model had been writing the backend’s source of truth all along, as ordinary blocks, with no notion that a backend would one day read them.
The materializer adds nothing the model can call. It walks the same tree the renderer walks, finds the declaration blocks, and projects them into Postgres DDL, the way the render pipeline projects a layout block into DOM nodes. When a user drags a table into their app, the schema block enters the CRDT through the identical path a paragraph edit takes. One sync layer carries it to every client. One undo layer can pop it back out. One render pipeline already knew how to draw the table on the canvas. The same tree is read three ways: built in the editor, edited by hand, and run against a live database.
Relations are the test of this. A foreign key here is a reference block pointing from one table block to another, the same reference the editor already follows when it renders a linked record inline. Triggers are blocks too, declarations of what should fire on a row change, written through the same tool that writes any other behavior. The materializer reads those and emits constraints and trigger functions. Nothing about persistence asked the model to learn a new move.
This is the property the sidecar would have severed, and it is real: a whole backend shipped as a new consumer of existing blocks rather than a new producer of anything. The change that added persistence touched the materializer and the registry, and never the 11-tool surface the model sees.
The property also has a bill, and it came due on the layer the whole design was founded on. The metacircular argument quietly assumes the substrate under the tree is load-bearing in proportion to how central it sits, and the CRDT sits at the very bottom. When the Yjs integration was finally audited, it touched somewhere between 10 and 50 lines that did anything vanilla JavaScript would not do trivially: a few merge points, a handful of observers, and beyond that plain object manipulation dressed up as collaborative editing. It was dropped. The foundational document type, the deep mechanism the no-new-primitives result was supposed to rest on, turned out to be load-bearing for far less of the system than its position at the bottom implied.