Skip to content

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 (Mongoose timestamps, owner, soft-delete isDeleted, approval releaseStatus) — the job is reconcile + fill three real gaps (explicit modifiedBy, 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)

AskedTodayPlan
CreatedOncreatedAtevery schema has timestamps: true (stored UTC)expose in serializer/DTO + surface in clients
ModifiedOnupdatedAt (same)expose + surface ("Updated 2d ago", already partly on the dashboard)
CreatedBy🟡 owner (Space/Project/Track); Comment.authoradd explicit createdBy (backfill = owner) for true audit
ModifiedBy❌ missingadd 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:

  1. Audit — provenance, system-managed, always-on: createdBy/createdAt, modifiedBy/updatedAt. Never user-edited.
  2. State — coarse on/off: Active / Inactive. Today's isDeleted is the Inactive terminal.
  3. Lifecycle status — workflow stage, user-driven: Draft / Published / Cancelled / Archived. A NEW statusCode per entity, sensible fixed defaults, customizable later.
  4. 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: string should be repurposed as statusCode (enum) or deprecated, not overloaded.

Audit fields — the cheap, no-fork win

  • createdAt/updatedAt already exist → expose in serializers + DTOs and surface in clients.
  • Add createdBy (ref User; backfill = owner) and modifiedBy (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 isDeleted as Inactive/terminal (trash recovery, doc soft-delete). Add no parallel statecode unless "archived but not deleted" becomes real — then fold Archived into statusCode, 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 statusCode against 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.locale prefs 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 releaseStatus stores display strings ("Under review"); new enums should be keys, labels resolved at render. (Migrate releaseStatus later 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 Comment text, 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."
  • 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) and modifiedBy stamped on the main PUT edit (actor threaded from @GetUser). createdAt/updatedAt already existed (timestamps) and serialize by default. nest build + jest 14/14 green. Remaining (follow-ups): surface "Modified by · when" in iOS/web (read-only); stamp modifiedBy on the secondary mutate paths (track cover/version/status/rename — updatedAt is already correct on those, only the actor is unset); optional createdBy/modifiedBy backfill script for old records.
  • Slice 2 — lifecycle statusCode (backend shipped 2026-06-26): shared src/shared/lifecycle-status.ts catalogs (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 loose Track.status and from approval. nest build + jest 14/14 green. Follow-ups: a GET catalog 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)

  1. Status model scope — LOCKED 2026-06-26: fixed sensible defaults now (per-workspace customizable catalog deferred). Shapes slice 2.
  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.ts anytime before clients hardcode them.
  3. statecode — keep isDeleted-as-Inactive (recommend) vs an explicit Active/Inactive/Archived field.
  4. Slice 1 (audit) — LOCKED + SHIPPED 2026-06-26 (backend; see §Sequencing).
  5. Status stored as key vs label — recommend key (i18n-ready); migrate releaseStatus later or leave.

Cross-references

Ctrl-Audio Platform Documentation