Custom fields are the usual ceiling on runtime schema changes. Actionary lifts that ceiling. A platform admin defines entirely new entities through the UI and the rest of the product notices.
The entity metadata layer is the source of truth for the SPA renderer, the admin REST API, the MCP tool surface, the workflow engine, the search index, the audit log, and the merge-time gates that keep the whole thing coherent. Add a Warranty Claims entity through Schema Admin: the list view renders, the modal forms render, the polymorphic pickers offer it, the agent’s tool catalog grows a matching shape, the workflow builder can trigger on it, the portal can accept form submissions against it. It ships as a data change through the admin UI. The app deploy is untouched.
This is the moat. Every agent capability on the /product/agent page, every signal on /product/signals, every workflow on /product/workflows rests on it.
One metadata payload, four consumers
The public entity-configuration endpoint returns a JSON Schema with per-field render kinds, FK targets, enum options, many-to-many shapes, and required-field flags. Four independent surfaces read the same payload:
- The SPA dispatches on render kind to render list pages, detail modals, and forms.
- The admin REST API routes read entity configuration to validate writes and shape responses.
- The MCP tool surface exposes the entity catalogue over MCP — the exact payload the embedded agent and any outside MCP client consume.
- The agent’s per-turn prompt injects a slim per-tenant catalog, memoised per schema version and invalidated on the next metadata edit, so it stops spending orientation tool calls on cold conversations.
The agent reads the same schema every other surface reads. Adding a field surfaces in every configurator automatically because there is nothing to update.
Schema Admin stamps physical schema and metadata together
When a platform admin adds an entity, a single transaction applies a migration to the physical database AND stamps the metadata rows. A new column exists on disk and in the metadata layer at the same commit. Coherence is a transaction boundary; the database enforces it directly, so no background reconciler needs to catch up.
Every entity table follows one canonical shape: primary key, tenant id, owner id, created-by, created-at / updated-at, and a nullable soft-delete timestamp. Three companion indexes. An ownership-mode setting on the entity decides how the router treats the owner column — single-owner stamps the caller on create, none skips the default. Migrations that carry a tenant id without the audit trio fail at merge time.
The Generic Repository reads shape from the metadata layer
Two categories of aggregate live behind the code. Entity-registered aggregates — accounts, contacts, deals, and any bespoke entity Schema Admin adds — share one Generic Repository that reads shape from the metadata layer and composes SQL against the physical columns Schema Admin stamped. Adding a new entity leaves the repository count unchanged. System aggregates — partners, inbound forms, deploy history, schema migrations, audit log — get one bespoke repository each.
Every SQL statement lives inside the repository layer; routers, application services, and helpers call repository methods, and the database sits behind that boundary. The rule runs on every pull request as a per-file budget check that fails the build on any inline-SQL regression. A regression cannot ship.
Cross-cutting concerns land as sidecars
Two polymorphic sidecars, each keyed by tenant plus entity plus record id, carry cross-cutting state on their own tables so every entity table stays clean:
- A sync-state sidecar carries external-system synchronisation metadata across every entity that syncs outbound. One shape, single-join read pattern.
- A currency-conversion sidecar carries the converted-to-functional-currency representation of money amounts across every entity that holds money.
One polymorphic relations table carries every many-to-many edge with a typed attribute payload — {"role": "billing_contact"}, {"weight": 0.8} — so one edge table serves every relationship type. One polymorphic grant primitive backs every share flow, with grantee kinds covering user, team, department, role, whole tenant, and external channel partner. One grant primitive; one live-row uniqueness constraint; every share surface in the product reads the same grants.
Three logging entities, attachable by one checkbox
Activities, Tasks, and Interactions are first-class entities. Any other entity in the system — built-in or admin-created — opts into being “loggable against” by ticking Enable Activities, Enable Tasks, or Enable Interactions in Schema Admin. The moment the flag flips, that entity appears in the logging entity’s Related To picker, its record-detail modal grows a timeline panel, and the agent’s link surface returns those records with the right role. It ships as a metadata edit; the app deploy is untouched. The polymorphic reference on the logging row accepts any entity plus record id; the visible picker respects three composing gates — capability flag (metadata), module enablement (commercial), per-role read scope (permission).
The Revenue module collapses the leads-plus-opportunities split into one unified deals entity with six seeded stages, three first-class lookups (deal sources, deal types, lost reasons), and a canonical closed-plus-won pair as the single source of truth for status.
An in-app runner with content-hash drift detection
Migrations are numbered SQL files. Platform admins apply them through the in-app migration runner. The runner reads files from disk, computes a content hash, applies under a database advisory lock, and records every apply with content hash, duration, applying user, and stdout. A hash mismatch flags drift; the runner refuses to silently re-run an edited file. Concurrent index builds route outside the advisory transaction because Postgres requires it. The statement splitter respects strings, dollar-quoted bodies, and comments.
Every migration declares a phase — additive, expand, switch, contract, or data. The deploy pipeline runs the pre-roll set (additive / expand / switch / data) before the rolling app cutover and the post-roll set (contract) after. Destructive verbs live only in the contract phase. A column rename is three migrations across two deploys — the industry-standard expand/contract discipline from Refactoring Databases (Ambler & Sadalage, 2006), applied cleanly. Deeper reasoning at /trust/design-principles.
An in-app entity-metadata health surface reads the physical schema back against the metadata layer and reports any drift — a column that exists on disk but not in metadata, or the reverse — with a one-click self-heal action for the trivially-recoverable cases.
The takeaway
A tenant adds a Warranty Claims entity through Schema Admin as a data change. The list view renders because the SPA dispatches on the render kind in metadata. The forms render for the same reason. The polymorphic pickers offer it because the logging capability flags flipped. The agent sees list, create, and update tools for warranty claims in its MCP catalog because the tool catalog is generated from the entity metadata. The workflow builder can trigger on it. The portal can accept submissions against it. The FK columns render as human labels through the same hydrator every list uses. The search-and-filter chips work because trigram search reads searchable and is-label flags off the metadata.
One source of truth, four consumers, zero drift. That makes every downstream capability — the ones the CTO evaluation page walks through end-to-end — a property of the substrate. Every capability the product ships follows from it.