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 | Public reads and a separate exact/hidden location-publication policy are implemented. |
| 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 packages/data-access/src/schema/constants.ts, with the Drizzle/Zod schema helper in packages/data-access/src/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 read helper:
isActiveEntity(table.entityStatus)The helper lives in packages/data-access/src/reads/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, Venue Index/Details/typeahead/map projections, and saved page-query preset listing. Event-series routes are still early, so their active-row query behavior should move 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.
Venue Location Publication Evidence
Section titled “Venue Location Publication Evidence”venues.locationPublicationCode is a content-governance policy distinct from row lifecycle and address truth. exact
permits a complete current coordinate pair to enter strict public map projections; hidden publishes no point. A new
Venue with a resolved, complete coordinate pair defaults to exact. Editors should choose hidden case by case for a
secret, private, TBA, or otherwise unsuitable public location. Coordinate-incomplete locations remain hidden, and
mutations reject exact without both coordinates.
The database column keeps an unconditional hidden default as a fail-closed persistence backstop. A table default cannot
inspect the referenced address row, so changing it to exact could create invalid publication state through a low-level
insert. Application create paths therefore choose the meaningful conditional default explicitly after location
resolution. Address correction, re-resolution, and relocation preserve the Venue-owned publication choice instead of
silently republishing an intentionally hidden Venue.
Changing this policy is a consequential human action, so venue_location_publication_audits retains actor, time, Venue,
mutation request, resulting revision, and previous/next code. It deliberately does not duplicate coordinates. These rows
are durable verification evidence and are not pruned like short-lived mutation receipts, whose purpose is safe network
retry replay. A future cross-entity human-readable audit trail may ingest or supersede this domain-specific evidence,
but current row provenance and retry receipts should not be stretched into that job.
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.