Skip to content

Content Entity Governance

This page names the small governance concepts Wavemap uses around primary content entities. It is deliberately a vocabulary page, not a full CMS workflow design.

Primary content entities currently include artists, events, venues, event series, users, and saved page-query presets. Supporting rows such as media records, links, lookup tables, and relationship joins may gain similar fields when a concrete workflow needs them, but they do not inherit every primary-entity convention by default.

The first implementation pass established this baseline:

Entity AreaLifecycle LocationProvenance LocationNotes
Artistsartists.entityStatusartistsWorked example for create/update provenance, active-row filtering, and authorization seams.
Eventsevents.entityStatuseventsSchema/query baseline in place; event time modeling is still deferred.
Venuesvenues.entityStatusvenuesSchema baseline in place; public read surfaces are still early.
Event seriesevent_series.entityStatusevent_seriesSchema baseline in place; public read surfaces are still early.
Usersuser_metadata.entityStatususer_metadataAccount lifecycle metadata lives beside account verification and security flags.
Saved query presetspage_query_presets.entityStatuspage_query_presetsActive filtering is applied to preset listing.

The source vocabulary lives in apps/wavemap-back-end/src/constants/entityStatus.ts, with the Drizzle/Zod schema helper in apps/wavemap-back-end/src/db/schema/entityStatusSchema.ts.

Use entityStatus for row lifecycle state.

The backend vocabulary currently allows only:

  • active
  • deleted

This keeps room for future states such as draft, published, or archived without introducing them before the product uses them.

Primary tracked rows carry this vocabulary in an entityStatus column. User lifecycle state currently lives on user_metadata, where account provenance and security flags already live. The same rows also reserve nullable deletedAt and deletedByUserID fields for future soft-delete behavior.

entityStatus is separate from domain-specific status concepts. For example, venue status, event-series status, event time status, cancellation, and future publication workflow statuses may each answer different product questions. Do not fold those meanings into lifecycle state unless the product language has genuinely converged.

Create paths for primary entities should set entityStatus: active explicitly. Database defaults remain useful as a backstop, but handler and seed code should make the convention visible at write time.

deletedAt and deletedByUserID are reserved metadata. Their presence does not mean a table currently supports soft delete.

Hard delete remains the active behavior for an entity unless that entity’s delete handler updates entityStatus to deleted instead of removing the row. For hard-delete actions, the future audit trail is the durable historical record; deletion metadata on the row disappears with the row and should not be treated as audit history.

When soft delete is introduced for a specific entity, prefer updating the row in place:

  • deletedAt
  • deletedByUserID
  • entityStatus = deleted
  • updatedAt
  • updatedByUserID
  • revision

Active-row uniqueness should be handled deliberately at that point, especially for slugs, public IDs, and relationship pairs. Active-row filtering should also move entity by entity with the concrete soft-delete behavior, not as a global assumption.

Public and user-facing read surfaces should filter primary rows through the shared backend helper:

isActiveEntity(table.entityStatus)

The helper lives in apps/wavemap-back-end/src/queries/queryControls/entityStatus.ts.

Apply this to implemented list, detail, typeahead, and query surfaces as those surfaces are touched. Do not retrofit every internal lookup blindly: mutation paths, admin repair tools, auth flows, and future audit/debug views may need to find non-active rows deliberately.

Current active-row filtering covers the implemented artist browse/details/typeahead surfaces, event query/typeahead surfaces, event-summary venue hydration, and saved page-query preset listing. Venue and event-series routes are still stubs, so their active-row query behavior should be added with their first real read implementations.

Direct row provenance is the lightweight metadata attached to an entity row:

  • createdAt
  • createdByUserID
  • updatedAt
  • updatedByUserID
  • revision

Use database timestamps as typed instants. Keep legacy public DTO fields such as createdOn and lastModifiedOn only as API compatibility adapters where needed.

revision is the row-level optimistic edit token. Increment it when a meaningful mutation updates the row. It is not a history table and should not try to answer who changed which field over time.

Current mutation behavior is last-write-wins. Handlers increment revision after a successful row update, but they do not yet require clients to send an expectedRevision value or reject stale edits with 409 Conflict. Add that optimistic-concurrency check only when an edit surface needs stale-write protection and can present a useful conflict resolution path.

Create handlers and seed adapters should populate the full provenance shape instead of relying on database defaults for application meaning. Legacy MongoDB seed JSON may still use createdOn, createdByID, lastModifiedOn, and lastModifiedByID; the seed adapter boundary translates those into the current names and timestamp types.

Update handlers should set:

  • updatedAt
  • updatedByUserID
  • revision = revision + 1

Only meaningful row mutations should advance revision. Relationship or media side effects may need their own row-level provenance later, but they should not pretend to be changes to the parent entity’s scalar profile unless the parent row itself changes.

For now, Wavemap is a platform-published content system. Admin-capable users can create and edit globally managed content according to route permissions.

Future Waveguide-style ownership should be modeled as content control, not as a replacement for association rights. A privately controlled artist profile may restrict who edits photos, descriptions, and profile details, while still allowing platform admins to associate that artist with events when they have relationship-management permission.

The current permission grammar is most explicit for artists:

  • update-artist-profile controls artist scalar/profile fields and profile links.
  • manage-artist-media controls artist media upload, deletion, ordering, and synchronization.
  • manage-artist-event-relationships controls artist-event associations.

Today the same admin-capable roles receive all three artist permissions, but keeping them separate preserves the future path where a user may manage event associations, media, or profile fields independently.

Artist mutation handlers also pass through backend authorization helper seams:

  • assertCanMutateEntity(...)
  • assertCanManageEntityMedia(...)
  • assertCanAssociateEntity(...)

For now these helpers delegate to role-derived permissions. Later they can inspect owner/controller state for the target entity while preserving the current handler shape.

Frontend authorization helpers use matching entity-shaped can... vocabulary for UI affordances. They are deliberately non-authoritative, but they prevent public-page controls from depending directly on today-only role assumptions.

Do not add owner columns until a concrete account-controlled workflow exists. Preserve the architecture boundary by keeping these concerns distinct:

  • Profile/content editing permission.
  • Media-management permission.
  • Relationship management permission.
  • Future profile control or ownership.
  • Public visibility and publication state.

The future audit trail should be append-only operational history. It will overlap with row provenance, but it should capture more context:

  • Actor
  • Action
  • Target entity type and identifier
  • Request or trace context
  • Before and after summaries where appropriate
  • Relationship and media side effects where appropriate

Row provenance answers “what is the current lightweight state of this row?” Audit history answers “what happened over time?” Keep those concepts aligned, but do not force one to substitute for the other.

Do not build audit behavior into row provenance fields. The useful foundation already in place is the handler shape: mutations now have an authenticated actor, a resolved entity reference, route-level permissions, and entity-shaped authorization helpers. That is enough for a future audit emitter to observe the same mutation context without changing every handler signature first.

The current governance pass intentionally does not settle:

  • Event time modeling, including local event date/time, IANA timezone, UTC instants, time TBA, doors/end semantics, and event-series recurrence posture.
  • Soft delete or restore behavior for any specific entity.
  • Active-row partial unique constraints after soft delete.
  • Account/org-controlled profile ownership.
  • Full audit event schema, retention, or admin presentation.

Those should move when a concrete product surface needs them. Until then, the current fields and helper seams preserve space for those decisions without asking every entity to implement them early.