ADR-0002. MetaLink links assets via polymorphic (principalType, principalId)
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-15 |
| Deciders | platform |
| Supersedes | — |
Context
- Many entity kinds need attached media —
Product,ProductVariant,Organizer,Category,Ledger(andMerchant/Userrelations). - Asset is a library mounted into multiple hosts; it cannot own a foreign key into every consumer's table without inverting the dependency direction.
MetaLinklives in@nx/coreprecisely so commerce/ledger can reference it without a circular dep — a hard FK back to those tables would reintroduce that cycle.
Decision
We will store the owner as a soft polymorphic pair: principalType (text, validated against MetaLinkPrincipalTypes) + principalId (text), with no DB foreign key. Drizzle relations() declares per-type one-to-one joins resolved on principalId.
Consequences
| Pros | Cons |
|---|---|
One MetaLink table serves all entity kinds | No referential integrity — orphans possible |
| No circular dependency between core and consumers | New principal kinds need a MetaLinkPrincipalTypes + relations() edit |
Composite index (principalType, principalId) keeps lookups fast | Type validity enforced in code, not the DB |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
Per-entity FK columns (productId, ledgerId, …) | Real referential integrity | Schema bloat; circular deps; edit every time | Defeats the shared-table goal |
| Join tables per entity | Clean cardinality | N tables to maintain; more joins | Over-engineered for 1:N media |
metadata jsonb owner ref | No schema change | Unindexed, unqueryable at scale | Composite index needed for lookups |
References
packages/core/src/models/schemas/public/meta-link/schema.ts(principalType,principalId, indexes)packages/core/src/models/schemas/public/meta-link/model.ts(polymorphicrelations())packages/core/src/models/schemas/public/meta-link/constants.ts(MetaLinkPrincipalTypes)