Skip to content

43 — Configurable Attributes — the "LEGO" attribute engine (envisioning)

Envisioning doc — needs confirmation. Governed by 00-ROADMAP.md. Created 2026-06-27 (owner-raised, split out of doc-42 §8). The ask — owner's "LEGO software": entities should be able to carry different attribute sets; the same attribute can live at different hierarchy levels; catalogs are per-workspace definable — so the platform models each customer's metadata, not one hard-coded shape. This is the composable data substrate under mgmt.ctrl and the consolidation of every deferred "customization" thread (doc-17 Lego OS, doc-20 §4, doc-41 per-workspace catalog, doc-31 monetization).

Three decisions locked at envisioning (2026-06-27, owner):

  1. Model = typed attribute bag + per-workspace definition catalog ("EAV-lite"). One whitelisted attributes object on entities, validated service-side against an AttributeDefinition catalog (key / label / type / level / options) — generalizing the existing lifecycle-status pattern (src/shared/lifecycle-status.ts). Not full per-value-row EAV; not dynamic Mongoose schemas.
  2. Scope = global presets + workspace override. Ship industry-standard default catalogs (works day one, no config), let a workspace tweak/extend/retire them; definitions inherit down the hierarchy. Matches doc-41 "fixed defaults now, customizable later."
  3. Authoring = both — inline quick-add a field from mgmt.ctrl + a full Workspace Settings catalog editor (types, levels, options, reorder, retire).

    Sharpened by doc-54 (2026-07-04): the inline quick-add "+" (the stakeholder's "bubble or plus sign") is THE primary authoring surface (Portals + mgmt.ctrl rows); the catalog editor is the secondary power surface. Default visible set = a minimal preset ([untitled]'s start-easy ethos, doc-15). Free tier: the "+" picks from the preset catalog (reading/using never gated — the rule below); defining new custom definitions stays the premium catalog-write.

Posture: envisioning only. This is a premium / post-MVP layer (doc-31 monetization). The default presets can ship as fixed catalogs early (cheap, like lifecycle-status); the configuration engine is the heavy part and does not block mgmt.ctrl (which consumes whatever attributes exist).

Why this exists (lineage)

Four docs have each parked a slice of "let the data be configurable." doc-43 unifies them:

SourceThe parked threadWhat doc-43 does with it
doc-17 §1 "Lego OS""composable blocks at any hierarchy level"The data expression of that ethos: attributes as blocks placeable at any level.
doc-20 §4customization/templates deferred (theming · saved views · role templates)Attribute catalogs are the schema half of that customization layer.
doc-41lifecycle statusCode "fixed defaults now, per-workspace custom catalog deferred" (lifecycle-status.ts:6)statusCode becomes the first instance of the general definition engine — same key/label/catalog shape, generalized.
doc-31monetization deferred; "pricing around team/roster value"Configurable attributes = a natural premium tier capability.
doc-42 mgmt.ctrlreads/writes "whatever attributes exist"; registry "per-workspace configurable later, same seam"doc-43 is that "later" — the surface doesn't change, the catalog becomes editable.

Finding — net-new, no foundation today (verified 2026-06-27)

  • All hierarchy schemas are fixed @Prop (ArtistSpace/Project/Track/File). There is no open attribute mechanism.
  • src/main.ts ValidationPipe is whitelist: true + forbidNonWhitelisted: true — it actively rejects any unknown top-level field on write. A dynamic attribute therefore cannot be a loose top-level prop; it must live in one explicitly-whitelisted bag, validated below the DTO layer.
  • Only seeds: an unused File.metadata: any (file.schema.ts:68); freeform tags: string[] (+ shareTags) on Track/Project/ArtistSpace/Comment; ShareTag {label,color} (per-user); the typed- but-empty Track.credits: []. None is a definition engine.
  • The precedent to generalize: lifecycle-status.ts already does "stable key + i18n label, per- entity catalog array, @Prop enum + @IsIn validation, render labels at display." doc-43 is that pattern, made data-driven and per-workspace instead of hard-coded.
  • No workspace/tenant container — the hierarchy is User → ArtistSpace → …. ArtistSpace is the de- facto tenant root (owner + collaborators + audit). The catalog needs a home (see §Catalog home).
  • Monetization rails exist: user.subscription (SubscriptionType Free / Explorer / Pro / Diamond) — the gate for premium custom definitions.

The model — typed bag + definition catalog

Two pieces: a catalog of definitions (what attributes exist) and a value bag per entity (what this record's attributes are).

1 · AttributeDefinition (the catalog entry)

Generalizes StatusOption {key,label} into a full field definition:

ts
type AttributeDefinition = {
  key: string;            // stable, immutable, machine id — e.g. 'mood', 'bpm', 'sync_cleared'
  label: string;          // i18n display label, resolved at render (doc-41 pattern)
  type: 'text' | 'number' | 'boolean' | 'date'
      | 'select' | 'multiselect'   // options[] required
      | 'url' | 'ref';             // ref → another node (artist/project/track)
  levels: ('artist'|'project'|'track'|'file')[]; // WHERE this attribute may appear (the "different
                                                 // levels" ask) — one definition, many levels
  options?: { key: string; label: string }[];    // for select/multiselect (same key+label shape)
  required?: boolean;     // enforced on the levels it applies to
  default?: unknown;      // seeded on create
  group?: string;         // UI grouping (e.g. 'Rights', 'Creative', 'Distribution')
  origin: 'preset' | 'workspace'; // preset = shipped default; workspace = customer-defined
  retiredAt?: Date;       // soft-retire: hidden from authoring, existing values preserved (read-only)
};

2 · The value bag on entities

One whitelisted field per hierarchy entity — the only top-level prop the ValidationPipe sees:

ts
@Prop({ type: Object, default: {} })
attributes: Record<string, unknown>;   // { mood: 'calm', bpm: 120, sync_cleared: true }

DTOs accept attributes?: Record<string, unknown> (whitelisted, @IsObject()); the real validation is service-layer — exactly like track.service.ts already normalizes tags on every update. The service checks each key against the workspace catalog: known key · applies to this level · type matches · option is valid · required present. Unknown/ill-typed → BadRequestException. This preserves the strict forbidNonWhitelisted security posture without loosening the pipe.

Why not full EAV / dynamic schemas? A per-value AttributeValue collection (one doc per value) buys cross-cutting query power we don't need yet and costs joins + volume; dynamic per-workspace Mongoose schemas are fragile to migrate. The bag+catalog is Mongo-native, indexable on demand (sparse indexes on hot attributes.<key>), and upgrades to materialized/EAV later only if reporting demands it (doc-14 scale-lever discipline).

3 · Attribute-at-different-levels (the core "LEGO" ask)

A definition's levels[] says where it may live. The same key can apply to artist and track; a value set at a higher level can be inherited as a default and overridden lower down (opt-in per definition: inherit: true). Example: genre defined at ['artist','project','track'] → an artist's genre cascades to its projects/tracks unless a track sets its own. This is the data analogue of doc-20's permission cascade.

Added 2026-07-05 (owner-raised) — file is a first-class attribute node. 'file' is already in levels[], but the owner flagged that Files / FileType / file metadata must be LEGO-configurable too (and that filter & search will need them — doc-40 pointer below). Concretely:

  • File.metadata: any (the seed, file.schema.ts:68) becomes the file attribute bag — the same typed-bag + catalog model as every other node, at the file level.
  • Built-in file facets ship as presets (like lifecycle-status, no config): kind (type = audio · video · image · attachment), attachment folder (Lyrics/Stems/Contracts/ custom, doc-45), format (contentType/MIME), size, duration, version, review status (doc-29). These are the always-there file attributes the catalog exposes read-only.
  • Custom file attributes (workspace overlay) ride the bag: e.g. "stem instrument", "contract status", "delivered?", "language" on a lyrics file — the lazy artist never sees them; a label/distribution studio defines them once (doc-31 gate on catalog-write, as everywhere).
  • FileType itself is extensible at the folder level (doc-45's folders already are custom per node); a workspace can define its own attachment folders/types beyond the preset three. This makes the asset cloud (doc-45) a fully-attributed layer — which is exactly what filter/search (doc-40) consumes.

Catalog home & scope — global presets + workspace override

SHIPPED DEFAULT CATALOGS  (origin: 'preset')      ← works day one, no config, free tier
        │  inherited
WORKSPACE CATALOG OVERLAY (origin: 'workspace')   ← add / relabel / retire / extend  (premium)
        │  inherited down the hierarchy
   Artist ──▶ Project ──▶ Track ──▶ File
        (a definition applies at the levels it declares; values inherit per §3)
  • Default presets ship as code-level catalogs (like lifecycle-status.ts) — sensible music-industry fields (e.g. mood, energy, BPM, key, language, explicit, sync-cleared, ISRC/UPC already exist).
  • Workspace overlay = customer edits. Home for the overlay: since no workspace container exists, scope it to the tenant root. Recommended: a lightweight WorkspaceSettings doc keyed by the owner/account (one catalog spanning all that owner's artists), not per-ArtistSpace (avoids roster- wide duplication). If/when a true Org/Label container lands (doc-17 fantasy), the catalog moves up without reshaping. statusCode (doc-41) becomes the first managed definition in this same overlay.
  • Resolution at read: effective catalog = presets deep-merged with the workspace overlay (overlay wins by key; retiredAt hides without deleting).

Authoring surfaces (both)

SurfaceRoleWhat
mgmt.ctrl inline quick-add (doc-42)Owner/Admin"[+ add field]" on a row/column → name + type + level, created in the overlay on the spot. Feeds the doc-42 registry so the new field appears as a column/filter immediately.
Workspace Settings — Attribute Catalog editorOwner/AdminFull management: create/relabel/reorder/group, set levels + options + required + default + inherit, retire (never hard-delete), preview impact. The "schema design" home.

Both write the same workspace overlay. Role-gated per doc-20; every change stamps audit (doc-41).

How the rest of the platform consumes it

  • mgmt.ctrl (doc-42): the view/widget/action registry becomes per-workspace configurable through the same seam — custom attributes show up as matrix columns, filter chips, group-by keys, and bulk- editable fields, with zero surface rewrite.
  • Sort/Filter/Search (doc-40): custom attribute keys become first-class filter/sort/group axes (filter: attributes.mood = calm, sort: attributes.bpm). Needs sparse indexes on hot keys.
  • Portals (doc-26): a node's Portal renders its applicable attributes (by level), grouped via group. Editing there writes the bag through the same validation.
  • i18n (doc-41): keys are stable/immutable; label/options[].label resolve at render per user.locale. Retiring/renaming never breaks stored values (keyed, not labeled).

Monetization gate (doc-31)

  • Free / Explorer: default preset catalogs only (read + use the shipped fields).
  • Pro / Diamond: the workspace overlay — define/relabel/retire custom attributes. Gate at the catalog-write service on user.subscription.name; reading/using existing values is never gated (so a downgrade degrades gracefully — values persist, editing the catalog locks).

Backend shape (name, don't build)

  • New attribute module: AttributeDefinition type + default preset catalogs (shared/), a WorkspaceSettings schema (overlay, keyed by owner), AttributeCatalogService (resolveEffectiveCatalog(owner) = presets ⊕ overlay) + validateAttributes(bag, catalog, level). Endpoints: GET /attributes/catalog (effective, per locale), PUT /attributes/catalog (premium, overlay CRUD), surfaced in Workspace Settings + the doc-42 quick-add.
  • Add attributes: Record<string,unknown> (whitelisted, default {}) to ArtistSpace / Project / Track / File schemas + update DTOs (@IsObject() only — deep validation in the service).
  • Service-layer hook: on every create/update of a hierarchy entity, run validateAttributes against the effective catalog for the owner + that entity's level — mirror the existing normalizeTags hook in track.service.ts.
  • Indexing: start unindexed (aggregate-on-read, doc-14); add sparse indexes on attributes.<key> only for keys that become hot filter/sort axes.
  • Migration safety: key immutable; type changes are a guarded migration (or a new key); retire = soft (retiredAt), values preserved; no destructive catalog edits.

Decisions resolved (2026-06-27, owner)

  1. Home = a dedicated WorkspaceSettings doc (not a field on User) — room to grow: doc-20 §4 theming / saved-views / templates share it. Keyed by owner/tenant; moves up cleanly if an Org/Label container ever lands.
  2. Preset fields are reconciled into the catalog, not duplicated — existing fixed @Props (ISRC/UPC/genre/releaseDate/…) become managed preset definitions rather than parallel fields. And not every field is required for every user/project — presets are available, applied per level and per required flag, so a solo artist never sees label-distribution fields they don't use.
  3. Cross-level inheritance is opt-in per definition (inherit: true) — off by default, on where it makes sense (e.g. genre artist→track).
  4. ref type is supported — attribute values may reference other nodes (e.g. "supervised by → user", "same session as → track"). Coordinate with doc-28 relations so the two don't double-model the same link (refs = lightweight attribute pointers; doc-28 = first-class typed relations).
  5. statusCode and tags fold in as managed definitions — the lifecycle status + tags become the first preset definitions in this engine (unifying), rather than staying bespoke fields. Migration keeps their existing storage/behavior; they gain catalog management for free.

Sequencing

Envisioning only — no build in this pass. Build order when greenlit (premium layer, after the mgmt.ctrl core + doc-38 backbone): (1) ship default preset catalogs as fixed code (cheap, immediate value, reconcile statusCode/existing metadata); (2) add the attributes bag + service-layer validation; (3) the WorkspaceSettings overlay + catalog endpoints (premium gate); (4) authoring surfaces (Workspace Settings editor + mgmt.ctrl quick-add); (5) doc-40 filter/sort/group on attributes

  • sparse indexes. mgmt.ctrl (doc-42) does not wait on any of this — it consumes the effective catalog, fixed-default today, configurable here.

Status: envisioning. Decisions 1–3 locked 2026-06-27. The default-preset slice is cheap and could ride earlier (like doc-41 lifecycle-status); the configuration engine + authoring are the premium, post-MVP body. Companion to doc-42; consolidates doc-17/20/41/31 customization threads.

Ctrl-Audio Platform Documentation