26 — Identity, Sharing & Navigation (envisioning)
Envisioning doc — needs confirmation. Governed by
00-ROADMAP.md. Created 2026-06-07. Answers the open question parked in25-IOS-CRUD-FIRST-RELEASE.mdand supersedes the parked Sharing section of23-IOS-PHASE2-IA.mdonce locked. Grounded in the real actors (12-ACTORS-WORKFLOWS.md) and the real backend. Nothing here is built yet — this is the model we agree on before building. Decisions to confirm are at the end.
What triggered this
A first-pass Shared tab was built and pulled — "Shared" isn't a tab, it's a consequence of how the account is structured. The owner's framing: a Workspace that may hold one or many Rosters; elements shareable outbound; inbound shares the recipient can organize (label / dot / bubble / color / team avatars); sharing visible down to the version; the Portal (metadata) surface on every element; and — because actor preference varies by role, culture, and generation — many ways to reach the same track-version: by hierarchy, by ownership/collaboration/viewer, by internal/external, by share direction (inbound/outbound), by #tags, and by custom filters / saved views.
Backend reality today (verified 2026-06-07)
- Hierarchy:
ArtistSpace → Project → Track → File (version) → Comment / Section. ArtistSpace is the top. - Sharing = collaborators (inline
{ userId, role }arrays on ArtistSpace/Project/Track;Role= owner/admin/editor/viewer; cascades down) + links (linkentity: per-resource share URLs with expiry/TTL/max-uses). Shareable granularity is theSharedResourceenum: ArtistSpace | Project | Track. - No Workspace, no Roster, no Team, no Tag entities. No "internal vs external" concept. These are net-new.
The backbone principle: one graph, many lenses
There is one object graph (the hierarchy above). Every way the user wants to "get to a track-version" is a lens — a saved filter over that one graph — not a separate copy or screen. The hierarchy is just the default lens. This is what makes the owner's long list of navigation axes cheap: each axis is a facet, not a feature rebuild. It's also how we honor per-actor preference without per-actor homes (consistent with the one-dashboard decision in 20) — the actor picks a default lens; the surface stays one surface.
Hard invariant: lenses filter; roles gate. A lens never grants access — it only reorders/filters what the user can already see via their collaborator role. Faceting must never become a permissions backdoor.
Layer 1 — Container model (the structural axis) — confirmed: lightweight
No new container entity above ArtistSpace. The hierarchy stays ArtistSpace → Project → Track → File(version). (Confirmed 2026-06-07.)
- "Workspace" stays conceptual — it's everything you own (all your ArtistSpaces + what's shared to you), not a stored entity. It's the mental boundary for "internal", not a schema layer.
- Roster = a label + color on ArtistSpaces — a soft grouping facet, not structure. A manager tags artists into colored rosters ("Pop", "Urban"); a solo artist never sets one and never sees the concept. Implement as a small field on ArtistSpace (e.g.
roster: { name, color }or reuse the tags layer) — no migration of the hierarchy. Rosters then become a lens (group/filter by roster) like any other facet in Layer 2.
This is the lightest path that still satisfies "one-or-many rosters": grouping is achieved by tagging, so there's nothing to collapse for solo users and nothing new to model server-side beyond a field.
Layer 2 — Access lenses (the navigation axis)
All over the same graph; combinable; each maps to how a real actor works:
| Lens | What it filters by | Primary actor fit |
|---|---|---|
| Hierarchy (default) | Workspace → Roster → Artist → Project → Track → Version | everyone; artists/producers |
| Role | ownership vs collaboration vs viewer-only | managers ("what I own"), guests |
| Internal / external | collaborators inside vs outside the Workspace | managers, A&R, legal |
| Share direction | inbound ("shared with me") vs outbound ("I shared") | everyone — replaces the pulled tab |
| Status | release status (no-status / in-progress / under-review / approved) | artists ("awaiting my approval") |
| #Tags | free tags on any node | power users; cross-project themes (e.g. #mixing, #single) |
| Saved views | a pinned combination of the above | the "custom views" ask; per-actor defaults |
Per-actor defaults (illustrative, user-overridable): Artist → Status: awaiting my approval + hierarchy · Producer/Engineer → outbound + needs-revision · Manager/A&R → cross-roster under-review + activity. These are defaults of one shared surface, not different apps.
Layer 3 — Sharing identity (the visual/semantic axis)
Two distinct things, both surfaced as badges (color / dot / bubble / avatars):
- Sharer-side identity (fixed): who shared this and as part of what — the collaborator/team avatars, the source roster/workspace. Answers "who gave me this?"
- Recipient-side organization (arrangeable): your own label + color on an inbound share, set by you, independent of how the sharer filed it (Gmail-labels / Finder-tags model). Answers "how did I file this?"
Surfaced on cards, rows, and version pills — down to the version — and editable in that element's Portal. Confirmed: a single version can be shared independently (you can send just v3) — so the backend SharedResource enum and the link entity extend to File/version granularity (see backend map). "Internal vs external" is itself a badge: since there's no Workspace entity, internal = a registered user you've added as a collaborator; external = guest/link access or someone outside your collaborator set (working definition — revisit if a real org/team entity lands).
Layer 4 — Portals everywhere (the universal inspector)
Generalize the Phase-1 Track Portal into the metadata + collaborators + share + identity surface for every node — Workspace, Roster, Artist, Project, Track, and Version. Same Portal / MetadataRow / LabelCopyTable components (already in DesignSystem/), node-specific content bodies. The Portal is where you see who it's shared with, set the recipient-side label/color, manage links, and edit metadata — uniformly.
How this maps to the backend (what's reuse vs net-new)
| Capability | Today | Net-new work |
|---|---|---|
| Hierarchy nav | ✅ ArtistSpace→…→Version | none — no container added (Roster is a field, below) |
| Roster grouping | ❌ | a roster {name,color} field on ArtistSpace + a group/filter lens (no hierarchy change) |
| Role lens | ✅ collaborator role arrays | client-side filter only |
| Share-direction lens | 🟡 inbound via /collaboration/collaborator/*; outbound derivable | optional /collaboration/shared-by-me aggregate |
| Internal/external | ❌ no concept | derive from collaborator-vs-guest (working def above); no entity needed |
| Tags | ✅ shipped 2026-06-18 — tags[] + shareTags on Artist/Project/Track, owner-private + opt-in, GET /search/tags aggregate, read-privacy interceptor | done (node tags); Comment.tags[] still deferred to doc-33 notes |
| Saved views | ❌ | a small per-user "views" store (can start client-only) |
| Sharing identity badges | ❌ | recipient-side label/color store; render badges on cards/rows/version pills |
| Portals everywhere | 🟡 Track only | content bodies for Roster/Artist/Project/Version |
| Version-level sharing | ❌ (SharedResource = Space/Project/Track) | confirmed target — extend SharedResource enum + link to File; collaborator/link resolution at file granularity (built when the sharing layer is built, post-CRUD) |
None of this blocks the CRUD slices (2–5 in doc 25) — Track/Project/Artist CRUD and audio upload are independent and can proceed in parallel while this model is refined.
Additions beyond the brief (proposed)
- Saved views as first-class — make the "custom views" ask concrete: a pinned lens-combo with a name, icon, color. Per-actor defaults seed it; users edit. The single highest-leverage way to serve culture/ generation differences without forking the UI.
- Activity lens — generalize "Recent" into a "what changed, by whom" feed; strong for managers/A&R.
- Teams (later) — "team avatars" implies grouping collaborators into named teams (a label's mixing team). Defer as an entity; for now badges use individual collaborator avatars.
- External-share hardening — once "external" exists, external links get safer defaults (shorter expiry, optional watermark/download-off) — a trust feature that sells to managers/labels.
Decisions — confirmed 2026-06-07
- Roster = a label/color on ArtistSpaces — not a new container. Grouping is a tag + a lens; no hierarchy change. (Layer 1 above.)
- Envision-only now — this model is locked on paper; the next build is CRUD slices 2–5 (doc 25). The sharing/identity/lens system is built after the create→review→approve loop is complete. Nothing in this doc ships yet.
- True per-version sharing — a single version/file can be shared independently; the backend
SharedResourceenum +linkextend to File granularity. Scheduled with the sharing layer (post-CRUD), not now.
Still open (smaller, decide when sharing is built): the precise internal/external definition if a real team/org entity is ever added; whether tags are workspace-global or per-resource; whether saved views start client-only or get a backend store. None block CRUD.
Build plan — phase kickoff (2026-06-12, owner: "go doc-26")
Backend-contract-first per the routing rule. Grounded findings: Link already has a file ref and resourceType is a loose string (per-version links are cheap); collaboration has full invite/collaborator/ link CRUD at all three levels; the real gaps are the aggregate, the tags store, and the roster field.
Wave 1 — backend contracts (Claude Code) — ✅ implemented 2026-06-12 (tsc clean; boot-verified:
all five routes mapped. Reaches TestFlight on the next container redeploy.)
- Roster tag on ArtistSpace —
roster?: { name: string, color: string }prop; accepted via the existingPUT /artist/:id; returned by all artist reads. Grouping/filtering is a client-side lens. - Recipient-side share tags (the arrangeable inbound identity) — new
shareTagcollection:{ user, resourceType ('ArtistSpace'|'Project'|'Track'), resourceId, label?, color? }, unique per (user, resourceType, resourceId). Endpoints:PUT /collaboration/my-tag(upsert; body{ resourceType, resourceId, label?, color? }),DELETE /collaboration/my-tag/:resourceType/:resourceId. Inbound reads (GET /collaboration/collaborator/{spaces,projects,tracks}) each gain amyTagfield. GET /collaboration/shared-by-me— the long-flagged outbound aggregate: my owned resources that have collaborators beyond me or active links →{ spaces, projects, tracks }, each entry with its collaborator list + link summary (count, last access).- Per-version share links —
SharedResourcegainsFile;POST /collaboration/share/file(makeFileShareableDto { fileId, role }) creates a link that grants the role on the parent track and preserves thefileref so clients deep-link to that exact version;GET /collaboration/link/file/:fileIdlists. (Resolution rides the existing/:linkId/add-userpath.)
Wave 2 — iOS slices (Xcode agent, one at a time via next-brief.md)
Shared screen v2 (inbound) — returns from the 2026-06-07 parking, now grounded: grouped inbound items with my-tag chips (label + color, editable in place / via Portal).✅ shipped 2026-06-12 (Xcode agent, build green) — newFeatures/Shared/SharedView.swift+Core/Models/SharedItems.swift, three sectioned feeds (Artists / Projects / Tracks) reusingArtworkHero/ProjectCard/TrackRow;MyTagChip(V#-pill language: 0.18-fill capsule in the tag's color);TagEditorSheet(labelTextField+ 8-swatch palette aligned toTheme.versionColor(for: 0..<8)+ Save / Update / Remove) opened via long-press context menu, routes throughPUT/DELETE /collaboration/my-tagand refetches.RootTabsViewis Home / Shared / Recents again. Lands on TestFlight after the next container redeploy.Outbound ("Shared by me") — off the new aggregate; link counts + collaborator avatars.✅ shipped 2026-06-13 (Xcode agent, build green) —SharedViewgot a segmentedPicker("With me" / "By me"); outbound segment is one fetch ofGET /collaboration/shared-by-mesectioned by Artists / Projects / Tracks, reusing the inbound tiles. NewShareSummaryLineunder each item: overlapping initials avatars (up to 3 ++Npill, accent-stable per user)- "
N link(s) · M opens" caption + a smallwaveformchip when any link carries afileref (a version is shared). Both segments fire in parallel on first appearance viaasync let; pull-to-refresh reloads only the active segment. Tag editing context menu stays on the inbound side only. Lands on TestFlight after the next container redeploy.
- "
Roster UI✅ shipped 2026-06-13 (Xcode) — name+color on an artist, Artists home groups by roster when any exist; solo-user invariant (no roster → no UI). Detail inCHANGELOG.md.Portals-everywhere✅ shipped 2026-06-13 (Xcode) —ArtistPortalContent+ProjectPortalContent(metadata, collaborators, share, my-tag, image/cover upload). Detail inCHANGELOG.md.Identity badges + share-this-version✅ shipped 2026-06-13 (Xcode) — Track Portal share + per-version share links; avatar stacks on Shared rows. Detail inCHANGELOG.md.
Wave 2 COMPLETE (2026-06-13), plus a polish/QA pass on top: hero cover display ✅, two-source image picker (Photos + Files) with confirm ✅, Photos-in-
Menuregression ✅. Two QA items re-opened (owner): per-track cover in the tracklist (was showing project art on every row) and the intelligent halo on all heroes (was version-only) — both pure-iOS, root-caused, re-queued in.agent/next-brief.mdahead of Wave 3.Validation gap — Shared (slices 5–6) is unconfirmed. The inbound/outbound Shared activity can't be verified from a single account; it needs a multi-account matrix — existing↔existing, invite-to-a-new account (signup→auto-add), and guest/link access (no account). This is a test-plan gap, not a code bug: seeded test accounts + a written run-through. Flag for when QA tooling lands; doesn't block Wave 3.
Wave 3 — lenses & saved views (client-first) — ✅ SHIPPED 2026-06-14 (Xcode; BuildProject green)
LensBar (Role · Status · Roster facets; solo-user-invisible) + LensManager saved views (persisted to UserDefaults, seeded "My Work" / "Awaiting Approval"), applied to Home / ProjectTracks / Shared. Detail in CHANGELOG.md. The #tags facet shipped as a disabled stub — it needed a backend tags[] store. Tags-scope DECIDED (owner 2026-06-14): tags[] are per-resource, owner-authored, private by default; they propagate to a recipient only via an opt-in "share tags" check, else none — not a global vocabulary. (Distinct from the already-built recipient-side my-tag.) See 33-NOTES-ANNOTATIONS.md §Decisions.
#Tags backend ✅ SHIPPED 2026-06-18 (Claude Code):
tags[]+shareTags(owner-private, opt-in) on Artist/Project/Track ·GET /search/tagsaggregate ·StripPrivateTagsInterceptorenforces read privacy. The decision above is honored at both the aggregate (query) and node-read (interceptor) levels. The iOS slice that un-stubs the facet is queued in.agent/next-brief.md.Comment.tags[](doc-33 notes) deferred.
doc-26 Waves 1–4 COMPLETE (backend + iOS) + the #Tags store. Remaining envisioned: doc-33 notes (
Comment.tags[]+ Portals notes band) and the Wave-4 extras (break-inheritance, etc.) — all owner-greenlit per phase.
Invariant throughout: lenses filter; roles gate — no lens ever widens access.
Wave 4 — Teams / Groups as share targets — ✅ SHIPPED (backend) 2026-06-17 (Claude Code; nest build clean, boot-verified — all 13 /team routes mapped)
What shipped: a new
teammodule (Team+TeamShareledger), 13/teamendpoints (CRUD · invite/accept/decline/remove · share/shares/unshare), and reusable id-basedgrant/revokeCollaboratorCascadeon the now-exportedCollaborationService. Grant model = fan-out + reconcile-on-join (TeamShare ledger back-grants late joiners, revokes on leave/un-share; no permission/read path changed). **Governance = owner
- admins; membership by consent (pending→accept).** Invariant preserved: a team lands only as standard per-resource collaborator roles, and only the sharer's owned resources can be shared in. iOS UI is the next slice (
.agent/next-brief.md). Scoped out (flagged): break-inheritance (orthogonal, platform-wide), File/version-level team share, "shared via Team" badge UI, and provenance-precise revoke (agrantedViamarker — v1 revoke is by the TeamShare resource list). Detail inCHANGELOG.md.
The envisioning that drove the build (kept for reference):
Today sharing targets individuals (email invite → collaborator) or open links. The owner's scenario: share an artist/project/track to a group at once, and — crucially — "the external user set the current user in a group, and the current user wants to share into that group." So groups must work across ownership boundaries, not just within one account. This is the B2B heart (doc 31): labels and managers think in teams, not individuals.
Proposed model — Team as a first-class, lightweight entity:
Team { name, owner, members: [{ userId, teamRole }] }whereteamRole ∈ { admin, member }. A team is a reusable set of people, not a permission by itself.- Sharing to a team = fan-out, not a new grant primitive. "Share project X to Team Y as editor" adds each current member to X's existing
collaborators(the role cascade already built) + records the team as the source (for the sharer-side identity badge / "shared via Team Y"). Members joining the team later get access via a resolve step (or a nightly/触发 reconcile). Invariant holds: the grant still lands as per-resource collaborator roles — teams are a convenience over the gate, never a bypass. - Cross-ownership (the owner's case): anyone can create a team and add others; membership is by consent (invite/accept, like collaborator invites today — you're not silently added to strangers' groups). To share into a team you must be a member of it (any member can share their own resources into a team they belong to; they can't grant access to resources they don't own — roles still gate). So: B creates "B's Crew", invites A; A accepts; A can now share A's track into "B's Crew" → every member gets the collaborator role on A's track. Reciprocal by construction.
- Identity: team avatars become the "team" badge doc-26 Layer 3 reserved; "internal vs external" can later mean shares a team with me vs not.
Decisions to confirm before building (not now — flagged):
- Team entity now, or keep individual+link sharing through first release? (Recommend: envision now, build as its own mini-phase after the current Shared waves 6–9 — it's a new entity + invite flow + fan-out/reconcile, comparable in size to Wave 1, and the per-individual sharing already covers the MVP.)
- Static fan-out vs live membership — snapshot members at share time (simple, but late joiners miss out) vs resolve team membership on access (always-current, needs a resolve hook on the collaborator/
sharedWithread path). Recommend live resolve for correctness, scoped to that wave. - Team ownership — single owner + admins (recommend) vs fully shared. Affects who can rename/disband.
Until built, the current per-individual + link sharing stands; nothing here blocks waves 6–9.
Share-link format — short codes + the /s/ route (owner-raised 2026-06-19)
Today every share link (artist / project / track / file) is {webOrigin}/s/{link._id} — a 24-char ObjectId. Two owner concerns, one worth building:
- ✅ Too long →
Link.shortIdshipped 2026-06-19 (Claude Code). Added a unique+sparse 6-charshortId(generateShortId()) set on share-link creation;findLinkByIdresolves byshortIdOR_idso new/s/xr7kq2links work and legacy/s/{_id}links still resolve. Remaining: the client should emitlink.shortIdin the share URL (one-liner); existing links keep their_idURL (re-share reuses them) — a backfill / lazy-assign on reuse is the optional follow-up. SeeCHANGELOG.md. - ℹ️ What
/s/means — keep it./s/is the universal share route ("s" = share); it is not artist-specific. TheLinkrecord carriesresourceType, so the server resolves artist/project/track/file from the code (the web has/shared/{artist,project,wave}landing pages behind it). An opaque, type- agnostic short path is the correct pattern (short, leaks nothing about the resource) — recommend keeping/s/, not making it type-specific. The only change worth making is the short code above; this entry just documents the convention so it stops being a question.
Cross-references
- First-release scope & slices:
25-IOS-CRUD-FIRST-RELEASE.md - Phase 2 browse (active; Sharing section parked):
23-IOS-PHASE2-IA.md - One-dashboard / role-driven:
20-UI-SIMPLIFICATION.md - Actors & workflows:
12-ACTORS-WORKFLOWS.md - Design system (Portal, badges, tokens):
22-IOS-DESIGN-SYSTEM.md