Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Dynamic entities

Understand the planned embedded mode where your application's own users define new record types at runtime, stored in one shared store instead of a table per type.

Some applications have to let their own users define what they record. An ERP built on Alvo has a customer who says “I need a register of company vehicles, with a plate number, a VIN and an owner”, and another who needs a register of service contracts. Nobody should run a schema migration for that, and the developer should not have to ship a release.

The obvious answer, a database table per user-defined type, breaks down quickly. A thousand customers with a dozen registers each is twelve thousand tables: the database’s catalog bloats, its planner and maintenance slow down, and every new register is live schema change, with its locks and its failure modes, triggered by an end user, repeatedly.

One shared store, never a table per entity

Section titled “One shared store, never a table per entity”

The planned design is the one Salesforce, Airtable and Microsoft Dataverse use: a metadata-driven store. A fixed, small set of tables holds everything, whatever users create:

  • the definitions of the record types and of their fields: names, types, which fields are required, references;
  • the records of every dynamic type of every tenant, in one shared table partitioned by tenant, each record’s values stored as JSON.

Creating a record type is then inserting a row of metadata: no schema change, no lock on existing tables, and ten thousand new types do not add a single database object.

The cost is honest too. Aggregations and joins over JSON values are slower than over real columns, so a type that grows large or reporting-heavy needs its hot fields indexed, or eventually its own table. Because the database enforces no types or references inside the JSON, the API layer has to validate every value against the definitions before it is written. And a dynamic type has no compiled C# type: it is read and written as JSON.

The part that is already built is the reason this can be added without rewriting Alvo. Everything above the data layer, the generated API, the rule engine and events, works from one abstract model of entities and fields that a schema registry supplies. Today it has one driver, which reads the physical tables Alvo created from the descriptor. The plan adds a second driver that reads the metadata tables and produces the same model.

So a dynamic entity is meant to be indistinguishable from a physical one to everything above the store: the same routes, the same CEL rules compiled to SQL, the same tenancy, the same events. The seam is already in place where it matters most: SQL for a rule is generated through a dialect interface that renders each field reference, so a field stored as a JSON path can be rendered there without touching the rest of the rule engine. The acceptance bar for the feature is that the same adversarial and policy test suite passes, identically, over physical and dynamic entities.

Dynamic entities are an embedded-mode feature, for a host whose end users create the types. The host decides the policy over that whole class of entities in the descriptor’s dynamicEntities block: whether it is enabled, a reserved name prefix so a user’s type can never collide with one the descriptor declares, the default rules every new type starts with, the field types users may choose, and limits on fields, records and types per tenant. Today that block is validated against the schema and then does nothing.