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.
Current Baseline
Section titled “Current Baseline”The first implementation pass established this baseline:
| Entity Area | Lifecycle Location | Provenance Location | Notes |
|---|---|---|---|
| Artists | artists.entityStatus | artists | Worked example for create/update provenance, active-row filtering, and authorization seams. |
| Events | events.entityStatus | events | Schema/query baseline in place; event time modeling is still deferred. |
| Venues | venues.entityStatus | venues | Schema baseline in place; public read surfaces are still early. |
| Event series | event_series.entityStatus | event_series | Schema baseline in place; public read surfaces are still early. |
| Users | user_metadata.entityStatus | user_metadata | Account lifecycle metadata lives beside account verification and security flags. |
| Saved query presets | page_query_presets.entityStatus | page_query_presets | Active 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.
Entity Status
Section titled “Entity Status”Use entityStatus for row lifecycle state.
The backend vocabulary currently allows only:
activedeleted
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.
Deletion Metadata
Section titled “Deletion Metadata”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:
deletedAtdeletedByUserIDentityStatus = deletedupdatedAtupdatedByUserIDrevision
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.
Active-Row Filtering
Section titled “Active-Row Filtering”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.
Row Provenance
Section titled “Row Provenance”Direct row provenance is the lightweight metadata attached to an entity row:
createdAtcreatedByUserIDupdatedAtupdatedByUserIDrevision
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:
updatedAtupdatedByUserIDrevision = 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.
Ownership And Control
Section titled “Ownership And Control”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-profilecontrols artist scalar/profile fields and profile links.manage-artist-mediacontrols artist media upload, deletion, ordering, and synchronization.manage-artist-event-relationshipscontrols 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.
Audit Trail
Section titled “Audit Trail”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.
Deferred Decisions
Section titled “Deferred Decisions”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.