41 — Records Discipline: Audit, State & Status (+ i18n foundation) (envisioning)
Envisioning doc — needs confirmation. Governed by
00-ROADMAP.md. Created 2026-06-26 (owner-raised). Add CreatedBy/On · ModifiedBy/On across the hierarchy; a state (Active/Inactive) and a status (Draft/Published/Cancelled… — customizable, with sensible defaults); timezone / i18n for international team collaboration. Finding: half of this already exists (Mongoosetimestamps,owner, soft-deleteisDeleted, approvalreleaseStatus) — the job is reconcile + fill three real gaps (explicitmodifiedBy, a unified lifecycle status distinct from approval, and i18n which is greenfield). This is the data foundation under doc-40 (richer filters / sorts / defined views) and search.
What triggered this
Owner: every hierarchy model should carry who/when created + last modified, a state (Active/Inactive) and a status (Draft/Published/Cancelled…, ideally customizable with good defaults), plus timezone/i18n awareness — so we get better filters, sorts, and defined views. (The pattern echoes Dataverse statecode/statuscode.)
Finding — most of it already exists, scattered (verified 2026-06-26)
| Asked | Today | Plan |
|---|---|---|
| CreatedOn | ✅ createdAt — every schema has timestamps: true (stored UTC) | expose in serializer/DTO + surface in clients |
| ModifiedOn | ✅ updatedAt (same) | expose + surface ("Updated 2d ago", already partly on the dashboard) |
| CreatedBy | 🟡 owner (Space/Project/Track); Comment.author | add explicit createdBy (backfill = owner) for true audit |
| ModifiedBy | ❌ missing | add modifiedBy — set on every mutating write (@GetUser). The real new work |
| statecode (Active/Inactive) | 🟡 de-facto via soft-delete isDeleted (queries exclude deleted) | keep isDeleted as the Inactive terminal; don't add a parallel field unless "archived ≠ deleted" is needed |
| statuscode (Draft/Published/…) | 🟡 Track has a loose status: string (free text, ~unused) + releaseStatus (approval, doc-29/32) | add a lifecycle statusCode enum, distinct from approval; fixed defaults now, customizable later |
| timezone / i18n | ❌ no user.timezone/locale (Dates are UTC, which is correct) | store UTC (done), render viewer-local; add prefs; label catalog deferred |
The model — four DISTINCT axes (do NOT overload one "status")
The biggest risk is conflating everything people call "status." Keep four orthogonal fields:
- Audit — provenance, system-managed, always-on:
createdBy/createdAt,modifiedBy/updatedAt. Never user-edited. - State — coarse on/off: Active / Inactive. Today's
isDeletedis the Inactive terminal. - Lifecycle status — workflow stage, user-driven: Draft / Published / Cancelled / Archived. A NEW
statusCodeper entity, sensible fixed defaults, customizable later. - Approval — review/sign-off dimension (doc-32): Under review / Approved / Changes. Stays separate.
One Track can be Active (state) · Published (lifecycle) · cover Under review (approval) · modified by Maya 2d ago (audit). Four orthogonal answers — don't cram them into one field. The existing loose
Track.status: stringshould be repurposed asstatusCode(enum) or deprecated, not overloaded.
Audit fields — the cheap, no-fork win
createdAt/updatedAtalready exist → expose in serializers + DTOs and surface in clients.- Add
createdBy(ref User; backfill =owner) andmodifiedBy(ref User; set on every mutating service write from@GetUser). Yields "Modified by Maya · 2d ago" everywhere + the "recently modified by me/X" filters/sorts. Clients consume read-only.
State & lifecycle status
- State: keep
isDeletedas Inactive/terminal (trash recovery, doc soft-delete). Add no parallel statecode unless "archived but not deleted" becomes real — then fold Archived intostatusCode, not a second boolean. - Lifecycle
statusCode— a per-entity enum with sensible fixed defaults (a fresh entity is never blank):- Track: Draft → Published → Archived (Cancelled optional)
- Project / Album: Draft → Active → Released → Archived
- ArtistSpace: Active → Archived
- Customizable later: shape it to allow a per-workspace label/option catalog (the Dataverse "status reason" idea) but don't build the config engine now — store a stable
statusCodeagainst a default catalog; a workspace-custom catalog is a deferred layer (matches doc-32 "customize later", doc-31 monetization-deferred). Ship default common labels so it works out of the box.
Timezone & i18n — greenfield, get the foundation right
- Store UTC (already true). Never store local time.
- Render in the viewer's timezone — clients format in device tz. For server-rendered output (emails, notifications, exports) add
user.timezone+user.localeprefs so each collaborator sees their local time — the "international team collaboration" need. - Status as a stable KEY, not a display string — so labels localize later with no data migration. ⚠ Flag: today's
releaseStatusstores display strings ("Under review"); new enums should be keys, labels resolved at render. (MigratereleaseStatuslater or leave as-is — decision 5.)
How it powers doc-40 (filters / sorts / defined views)
Exactly the keys the owner identified as missing:
- Sorts: recently modified (
updatedAt) · recently created · by status. - Filters: status = Published / Draft · state = Active · modifiedBy = me · createdBy = X.
- Defined views (doc-26 saved): "My drafts" · "Recently modified" · "Published this month" · "Awaiting me."
Search (the owner's "you tell")
The search module today indexes ArtistSpace / Project / Track names + tags only — comments/notes and portal metadata are not covered (the gap the owner named).
- Extend the index to comments/notes + portal metadata (the
Commenttext, scoped by access). - Two surfaces, two intents:
- Per-view
.searchable— scoped, in-context filter (doc-40 slice 2). - A global Search view — cross-hierarchy, incl. comments/notes; a dedicated surface from the dashboard. Yes, this one is a view — "find anywhere" is a distinct intent from "filter this list."
- Per-view
- Results filterable by the new status/state/audit fields → "find 'bridge' in comments on Published tracks modified this week."
Sequencing
- ✅ Slice 1 — audit (backend shipped 2026-06-26):
createdBy+modifiedBy(ref User) added to ArtistSpace / Project / Track; set on create (= owner) andmodifiedBystamped on the mainPUTedit (actor threaded from@GetUser).createdAt/updatedAtalready existed (timestamps) and serialize by default.nest build+jest14/14 green. Remaining (follow-ups): surface "Modified by · when" in iOS/web (read-only); stampmodifiedByon the secondary mutate paths (track cover/version/status/rename —updatedAtis already correct on those, only the actor is unset); optionalcreatedBy/modifiedBybackfill script for old records. - ✅ Slice 2 — lifecycle
statusCode(backend shipped 2026-06-26): sharedsrc/shared/lifecycle-status.tscatalogs (stable keys + default labels): Artist = active/archived · Project = draft/active/released/ archived · Track = draft/published/archived.statusCode(enum + default) added to all three schemas (fresh record defaults to active/draft, never blank); settable via the update DTOs (@IsIn-validated on the ArtistSpace class DTO). Kept distinct from the legacy looseTrack.statusand from approval.nest build+jest14/14 green. Follow-ups: aGETcatalog endpoint (so clients render labels / "customize later"); wire the doc-40 status filter/sort; surface a status pill in iOS/web; relax the enum when the per-workspace custom catalog ships. - Slice 3 — i18n prefs:
user.timezone/locale+ server-rendered local time; status stored as keys. - Slice 4 — search: extend to comments/notes + the global Search view (doc-40 slice 2/3).
Every backend change reflects in front + iOS (CLAUDE.md contract rule). Build after the active doc-37 work; slice 1 is independent and safe to start whenever greenlit.
Decisions — to confirm (flagged)
- ✅ Status model scope — LOCKED 2026-06-26: fixed sensible defaults now (per-workspace customizable catalog deferred). Shapes slice 2.
- ✅ Default status sets — SHIPPED with the proposed catalogs (Artist active/archived · Project draft/active/released/archived · Track draft/published/archived). Owner can adjust the labels/keys in
src/shared/lifecycle-status.tsanytime before clients hardcode them. - statecode — keep
isDeleted-as-Inactive (recommend) vs an explicit Active/Inactive/Archived field. - ✅ Slice 1 (audit) — LOCKED + SHIPPED 2026-06-26 (backend; see §Sequencing).
- Status stored as key vs label — recommend key (i18n-ready); migrate
releaseStatuslater or leave.
Cross-references
- The filters / sorts / views this feeds:
40-SORT-FILTER-SEARCH.md - Approval — the separate review dimension:
29-VERSION-SCOPED-APPROVAL.md·32-APPROVAL-MODEL.md - Data source-of-record + identity ladder:
21-DATA-ARCHITECTURE-AND-IDENTITY.md - Soft-delete (the Inactive state) + search module to extend:
ctrl-audio-backtrashSpace·search - Customize-later / monetization-deferred posture:
31-PRODUCT-VISION-360.md - Surfacing "modified by · when":
37-HOME-DASHBOARD.md