The Small Extra Thing
A client with a working system asks for something small. They want to track machine maintenance next to their orders. Which machine, last service date, next service date, who did it, a note, and a view showing what is due this month.
You do the honest estimate and it comes out at three weeks, and the client’s face does the thing faces do.
They are not wrong to be surprised. Described in words it is one table with six columns. The three weeks are not the six columns. They are the migration, the model, the create and update and delete endpoints, validation with errors that land on the right form field, the list page with sorting and paging and a search box, the detail page, permissions per role, the filters, the export, the audit trail, the tests, and the place in the navigation where it has to appear.
The part anyone asked for is small. The pile behind it is what the estimate is made of.
Now count how much of that is specific to machine maintenance.
The answer is the six columns. Everything else is the same work you did for orders, and for customers, and for quotes, and for the tickets module last spring. You have written the same list page eleven times with different nouns in it.
The Category That Solved This Decades Ago
There is a family of business systems that took the other route, and they were right about the structure even where they are wrong about other things.
Their properties are worth naming plainly, because they explain why companies stay on them for fifteen years. Everything is one coherent system rather than nine disconnected subscriptions that each own a slice of the same customer. Every object in it is modelled the same way, so the person who learns one screen has learned all of them. It can be customised per company without forking the product, so your specifics do not turn into a maintenance burden nobody will carry. And it stays yours: the model, the data and the extensions are things you hold rather than rent.
What we did not want was the weight, the vocabulary, and a stack we would be a guest in. So we built the same shape on our own foundations, and called it It’s About Business.
The Domain Is Data, Not Code
There is no orders table. There is no customers table. There is no maintenance table.
There are entity definitions, field definitions, view definitions, and records whose contents are JSON keyed by field. One generic API serves every entity. One generic renderer draws every screen. A field’s type - text, number, enumeration, relation to another entity, computed formula, rollup over child records, sequence - decides how it validates, how it renders in a form, and how it displays in a list, and that decision lives in exactly one type registry rather than in each page.
The consequence is the point. Adding maintenance tracking does not add a migration, an endpoint, a page or a test. It adds definitions. The list view, the detail form, the validation, the search, the audit trail and the navigation entry come from the engine, because they came from the engine for everything else too.
Three weeks becomes an afternoon.
Not because anyone worked faster, but because most of the work stopped existing.
Packs, and Why They Compound
Definitions are shipped as packs: plain TypeScript data declaring entities, fields, views, seed records, automations and menu items. Installing a pack upserts all of that in dependency order.
The interesting part is that packs stack. A base pack provides a capability - “there is a thing here that is a customer” - and other packs declare that they require it and then graft their own fields and views onto that same shared entity. Install a support pack next to a quoting pack and both attach to one customer, not to two customers with a synchronisation problem between them.
Every field and view row is tagged with the pack that contributed it. Re-syncing one pack replaces only its own contribution and leaves everyone else’s alone. A row with no pack tag is a customer’s own change, and pack synchronisation is required to preserve it.
That last sentence is the whole “customisable without forking” property, expressed as a nullable column. A company adds three fields of its own to the invoice entity. The vendor ships an upgrade to the invoice pack. The upgrade replaces vendor fields and does not touch the three. Nobody merges anything.
Two Invariants We Learned the Hard Way
Definition sync must be atomic. Replacing a pack’s fields is a delete followed by a create, and an early version of that ran outside a transaction. A draft pack failed validation halfway through installing, and a live entity was left with its fields stripped. The records were all still there. Every column was blank. It looked exactly like data loss, and for about ten minutes everyone believed it was.
Which brings us to the second one.
Records and definitions are independent. Field definitions never read or write record data. Drop a field and the records keep the key in their JSON. This is why that incident was recoverable in a single re-install: the definitions came back and the data was untouched underneath the whole time.
Seeding is idempotent for the same reason. If an entity already has records, seeding does nothing. Re-installing a pack is therefore the standard repair procedure rather than a dangerous last resort, which is a good property for a repair procedure to have.
Where It Fights Back
This is not free, and the failure modes are specific.
A machine that makes the common case cheap makes the uncommon case awkward.
A generic renderer has a ceiling. Ninety percent of business screens are a list, a form and a board, and the engine does those better than a bespoke page would, because improvements land everywhere at once. The remaining ten percent are genuinely bespoke, and every one of them is a fight. You either extend the engine for a case that has one user, or you accept a screen that is 80 percent right. Both answers are sometimes correct and neither feels good.
Querying JSON needs care. Server-side list, search, sort and facets over a JSON column work well and are not free. Indexing strategy becomes a real subject rather than something the ORM handles for you.
Computed and rollup fields are evaluated per page of results. That bounds how expensive they are allowed to be. A rollup that aggregates a thousand child rows per record is a design mistake you will discover at page load.
Debugging moves up one level. When a screen is wrong, the bug is rarely in the screen. It is in a definition, or in the pack that wrote the definition, or in the order two packs were installed. That is a genuinely harder thing to reason about than a component with a bug in it, and it is a real tax on everyone who is not the person who built the engine.
Building an engine takes far longer than building the application once. If we had needed one system for one company, this would have been the wrong call by a wide margin. It only pays back across many entities, many packs, and years.
One Deliberate Restriction
Schemas can be drafted by a model. Describe what you need and an agent produces a pack, which you then refine and install.
What the product deliberately does not have is live editing of entity, field and view definitions in the interface. No admin screen where a user adds a field to a live entity by clicking.
That restriction is unpopular in the first demo and correct by the third month. Definitions are code-shaped even though they are stored as data: they need review, versioning, a diff, and the ability to install the same thing into a test workspace first. Systems that let anyone add a field on a Tuesday afternoon end up with entities carrying fourteen fields, four of which mean the same thing, and nobody can say which.
A pack is a reviewable artifact. A click is not.
The Question to Ask
Not “should we build or buy.” The useful question is: how many distinct business objects will this company need modelled over the next five years, and how much do they differ from what any product ships by default?
One object with a standard shape: buy it. A handful with standard shapes: buy the coherent system and configure it.
A dozen objects, several of them specific enough that no default fits, and a company that intends to keep changing them: the price that matters is not the first module. It is the eleventh. Make that one cost an afternoon and the arithmetic on everything else changes.
It’s About Business is where ours ended up.