Context
A fashion edition needs apparel attributes on the commerce product model, size and fit on the variant, and its own tabs inside the commerce product screen.HasCustomFields (0116) is
tenant data, bounded and untyped in code, so it cannot carry an edition’s typed, indexed, validated fields. The
overrides tree (0306) lets an app
shadow a file it consumes, but a base product’s migrations are frozen snapshots
(0159) that no consumer may edit, and a sibling module may
touch another module only through its public groups (0294
decision 24). An edition needs a typed way in that keeps the base intact.
Decision
The word is “addition”: 0294 decision 23 reserves “extension” for an extension point’s extensions.-
A model addition is a class. An edition module declares
extends ModelAddition(Product)({ ... })in its ownmodels/group, in a<base-model>.addition.tsfile, with typed fields written as any model’s fields are (0133).Productis the base model imported from the base product’s released client package, never from its source. -
Storage is a side table in the edition’s schema. Gen writes the addition’s columns to a table in the edition
module’s own Postgres schema, keyed one-to-one by the base record’s id, with the base’s tenant column and
row-level security. The base product’s tables and migrations do not change; an edition migration is the edition’s
own, ordered after the base’s by
after:(0159). -
Fields appear under the edition’s key. On the wire, the addition’s fields are read and written through the
base model’s own queries and actions, nested under the edition module’s key (
muslin: { fit, season }), so a base client that knows nothing of the edition ignores them and an edition client types them. Validation runs as for any field, and the addition’s fields take the base record’s permissions and record rules (0333); an edition adds a permission only for a screen or an action of its own. -
Screens go through slots and placements. An edition module places its tabs, widgets and actions in the base
product’s resource slots through the placements of
0307, and gen adds the addition
fields to the base resource’s table, form and infolist builders as columns and fields the edition declares in its
own
resources/group. Those placements sit in a new layer of 0306: layer 2b, the edition’s modules, above the host module (layer 2) and below the app’s overrides (layer 3). An edition never shadows a base file; it places and extends. -
Events in, events out. An edition reacts to the base’s integration events with
Listenerclasses and emits its own events under its own module key (0141). It never subscribes to the base’s domain events or lifecycle events, which are private to the base. -
Versioned against the base. An addition is valid for the base version range the edition declares
(0335
decision 2). A base rename carries
@Renamed(old)(0294 decision 20), and gen fails the edition build when an extended model or field no longer exists in the base version it builds against. -
Tenant custom fields stay separate.
HasCustomFieldsremains the tenant’s typed-at-run-time addition, in its side table. An edition’s fields are code; a tenant’s are data. The two do not share a table or a namespace.
Consequences
- A base product can be extended by several editions at once, each in its own schema and under its own key, and a base upgrade breaks an edition only at the fields it renamed or removed, which gen names.
- An edition’s screen is a placement, so a tenant’s layout copy and a user’s overlay (layers 4 and 5) still apply on top of it.
- The base’s wire shape grows a namespaced object per installed edition; a base client that ignores unknown keys keeps working.
Enforcement
- planned — gen (STA-340):
addition_target_missingwhen the extended model or field is not in the base version;addition_key_collisionwhen two editions use one module key; an addition that imports the base model from source fails the boundary check. - planned — a conformance test in
@stackra/model: an addition’s fields round-trip through the base’s get and update with the base record’s permission, and are absent for a caller without the edition installed.