28 — Recordings, Variants & Releases (envisioning)
▶ OPTION C BACKEND BUILT 2026-06-28.
TrackgainedvariantType(original · radioEdit · explicit · clean · instrumental · remix · live),relations: [{track, type}](alternateOf · remixOf · sameRecordingAs, mirror-written both ways) andappearsInProjects[](secondary release membership). Endpoints:GET/POST/DELETE /track/:id/relations·POST/DELETE /track/:id/appearances·variantTypeviaPUT /track/:id.nest build+jest+ e2e green. Remaining = iOS (variant chips
- group-by/cross-link lenses, doc-26) and Option B (formal Release/UPC) if distribution is later formalized. Also unblocks doc-46 per-variant (
appliesToVariants). SeeCHANGELOG.md.Envisioning doc — needs confirmation. Governed by
00-ROADMAP.md. Created 2026-06-10. Explores how Sonnance should model the music-industry reality the owner flagged: remixes, radio edits, explicit/clean versions, and the same track appearing on multiple albums/compilations — and how to navigate between them (search · group-by · cross-reference hyperlinks). Composes with the "one graph, many lenses" backbone from26-IDENTITY-SHARING-NAVIGATION.md. Nothing built yet — decisions to confirm at the end.
The problem (what the owner spotted)
Real catalogs aren't a clean tree. One "song" spawns:
- Variants / edits — radio edit, explicit, clean, instrumental, remix, live. Distinct deliverables (usually distinct ISRCs), not revisions of a work-in-progress.
- Multiple appearances — the same recording shows up on the original single, the album, and a compilation / "best of". A many-to-many between recording and release.
Users then need to get between these: search, group-by, and cross-reference links that hop to "the explicit version", "the remix", "the album this also appears on".
Why today's model can't express it (verified against the schema)
Sonnance is ArtistSpace → Project → Track → File(versions):
Trackconflates "the song" with "a specific recording." ItsFile[]"versions" are production revisions (v1/v2/v3 mixes — the review loop), not variants. A radio edit is not "v4 of the mix."Track.projectis single-valued (required), so a track cannot belong to two releases — multi-album / compilation membership is unrepresentable.- Useful raw material already exists:
label(string),ISRC(per-recording id),UPC(per-release id),genre,releaseDateon Track. So the fields already hint at the recording/release split.
The industry frame (for shared vocabulary)
Standard model (MusicBrainz / DDEX-flavored):
- Work — the composition (the song as written).
- Recording — a specific recorded performance; carries an ISRC. Radio edit / explicit / clean / remix / live = different recordings of the same work.
- Release — a published package (single / album / compilation); carries a UPC.
- Track-on-release — a recording's appearance on a release at a position. This is the many-to-many join.
Sonnance's Track today ≈ a Recording; Project ≈ a Release-ish container; File[] = production revisions (a Sonnance-specific, valuable concept the standard model lacks).
Three options
A — Labels-only (lightest). Add a variantType tag + lean on label; express relationships only as free text. Cheap, but "appears on multiple albums" stays unrepresentable and cross-links are manual. Too weak.
B — Full Recording/Release remodel (most correct). Introduce real Release (UPC, many-to-many to recordings) and a Work grouping; rename Project→Release. Models reality precisely, scales to distribution/DDEX — but a large remodel + migration, over-built for this stage.
C — Pragmatic middle (recommended). Keep the hierarchy; add two small things to Track and lean on doc-26 lenses for navigation:
variantTypeenum on Track —original | radioEdit | explicit | clean | instrumental | remix | live(+ existinglabelfor finer free-text). Per-variantISRCalready supported.Bidirectional cross-references —
relations: [{ track, type }]wheretype ∈ { alternateOf, remixOf, sameRecordingAs }, andappearsInProjects: [projectId]as a secondary membership (the canonicalprojectstays the home; this adds extra appearances). Writes mirror both directions so the hyperlink works back and forward.Navigation falls out of doc 26's "one graph, many lenses":
variantType+label+relationsbecome tags + lenses + cross-link chips — search, group-by (variant / label / release), and tappable "Explicit" / "Radio Edit" / "Also on: Compilation X →" chips that hop between related items. No new navigation engine — it reuses the lens model.
Recommendation: C. It serves every case the owner named (remix, radio edit, explicit, multi-album, cross-reference) with modest backend (one enum + two array fields on Track + mirrored-write logic), keeps the review loop intact, and leaves a clean path to B later if distribution/UPC management is formalized.
Backend map (reuse vs net-new, for Option C)
| Capability | Today | Net-new |
|---|---|---|
| Production versions (v1/v2/v3) | ✅ File[] | none — unchanged |
| Variant type (radio/explicit/remix…) | ❌ | variantType enum on Track (+ reuse label) |
| Per-variant ISRC | ✅ ISRC on Track | none |
| Cross-reference between tracks | ❌ | relations: [{ track, type }] + mirrored writes; a resolve/expand on read |
| Appears on multiple releases | ❌ (project single) | secondary appearsInProjects: [projectId] (canonical project stays home) |
| Search / group-by / filter | 🟡 search module exists | surface variant/label/relation as lenses (doc 26) |
| Formal Release (UPC, true M2M) | ❌ | only if Option B is later adopted |
How this sequences
This is a new big capability, like sharing (doc 26) — envision now, build later. Order on the roadmap: polish/stabilize (current) → real-user feedback → then the deferred systems (sharing doc 26, variants doc 28), sequenced by what feedback shows users actually reach for. One cheap piece could ride along earlier: the variantType tag is trivial and would pair naturally with the cover/metadata polish if you want an early taste — flagged as optional, not now.
Cover art & releases (envisioning addendum, 2026-06-12)
Owner's questions from the cover slice: do covers have versions? which version is this cover? does the Portal hold only the single's art, or album/compilation art too?
- Covers already have versions.
Track.image[]is a history — every upload pushes a new image and flipsimageFinal(exact mirror ofaudio[]/audioFinal). "Which version is this?" =imageFinal. The Portal currently shows only the final; a cover history (browseimage[], set an older one as cover) becomes possible the moment the backend set-cover-from-existing verb lands (✅setCoverImageshipped 2026-06-12 — promote; the delete-from-history half is still queued). Per-cover approval already has its slot — thereleaseStatus.coverdimension — and could later adopt the doc-29 per-file pattern if cover review needs the same rigor as audio. The cover-approval home and this whole dimension are now grounded in32-APPROVAL-MODEL.md(owner: "cover approval belongs in the Single/Project/CD area, not the Wave"). - ⚠ Field note (owner, 2026-06-30) — readiness still counts cover PER-TRACK. The Project Portal shows "Cover approved 1/5" — the doc-32 readiness rollup (
computeReleaseReadiness→trackApprovedCount('cover')) treats cover as a per-track dimension, which contradicts the locked decision below (cover = release/project art, not per-track). To reconcile later: for Album/Compilation projects, the cover readiness step should be project-level (one project cover) not an N-of-tracks count; for Single, the track cover = the single's art (today's count is fine). Owner flagged as "consider afterwards" — captured here, not yet fixed. - Cover ownership follows the release. (Owner reconfirmed 2026-06-12: "cover is more from the project rather than the track" — Project/release art is the primary concept; Project-Portal art ships in the doc-26 phase.) A cover is release art, so it attaches to the container that represents the release:
- Project of type Single → the track's cover is the single's art (today's model — correct as-is).
- Project of type Album/Compilation → the Project owns the album art, managed in the Project Portal (lands with Portals-everywhere, doc 26). Track-level cover then acts as an optional per-track override (e.g., a focus-track single art inside an album).
- Display preference everywhere: track cover → project art → identity-accent gradient.
- With Option C's multi-release membership (
appearsInProjects[]): a track appearing on several releases shows that release's art in that release's context — covers belong to the relationship's release side, never duplicated onto the track. (This is why cover ownership must sit with the Project/release before multi-membership ships.)
Decisions — confirmed 2026-06-10
- Model depth = Option C.
variantTypeenum + bidirectionalrelations+ secondaryappearsInProjects[]on Track. Not a full Release/Work remodel (B), not labels-only (A). - Multi-album membership = secondary
appearsInProjects[](canonicalprojectstays the home; extra appearances are a lightweight M2M). Part of Option C. - Timing = envision-only. Built after the current polish pass + the sharing system (doc 26). The cheap
variantTypetag was not pulled forward — it ships with this doc's phase, not the current polish.
When this is built it goes backend-contract-first (the enum + two array fields + mirrored-write/resolve logic on Track — Claude Code's part) then iOS (variant chips, group-by/cross-link lenses), per the handoff rule for backend-touching slices.
Cross-references
- Navigation backbone (lenses, tags, cross-links):
26-IDENTITY-SHARING-NAVIGATION.md - First-release scope (CRUD complete):
25-IOS-CRUD-FIRST-RELEASE.md - UX backlog (cover/metadata polish that
variantTypecould ride with):24-UX-BACKLOG.md - Data model reference:
02-DATA-MODEL.md