Drafts and publishing
Every entity owned by Vox (IVoxEntity) has a published view and can have one editable draft. This lets an admin UI,
a PIM built on Vox, or another purpose-built editor save work without changing what production consumers see.
External entities (IVoxExternalEntity) do not have drafts: their lifecycle belongs to the source system and Vox
mirrors the versions it imports.
Published content, drafts, and perspectives
Published content is the live version. Draft content is a materialized preview of the published entity with pending changes overlaid on it. When no explicit draft exists, a draft-mode read falls back to the published entity. After a draft is published or reverted, Vox keeps that fallback behavior but no longer reports a pending draft.
An entity may also exist only as a draft. It is absent from published reads until it is published. Creating an entity
publishes by default; callers must explicitly choose Draft to create a draft-only entity.
Vox exposes four read modes:
| Mode | Result |
|---|---|
Published | Only live content. This is the default. |
Draft | The explicit draft when one exists, otherwise the published version. Draft-only entities are included. |
Release | The entity version in one content release, otherwise the published version. Requires a release ID. |
Both | An envelope containing published, draft, and draftInfo. The draft member is null when nothing is pending. |
draftInfo contains the ordinary draft version, last editor, changed field groups, schedule, and publication error or
conflict details. Both is intended for entity editors. Release membership and conflict metadata are exposed by the
content release API instead.
Saving through .NET
The updater remains publish-first for compatibility with ordinary application code. Pass a save mode when draft behavior is intentional:
var product = await updater.CreateAsync(
new ProductActions.Create { Id = "mug", Name = "Mug" },
VoxSaveTarget.Draft,
cancellationToken: cancellationToken);
var edited = await updater.UpdateAsync(
"mug",
new VoxContentVersion(VoxContentMode.Draft, product.Version),
new ProductActions.SetName { Name = "Stoneware mug" },
VoxSaveTarget.Draft,
cancellationToken: cancellationToken);
VoxContentVersion says both which version the command was based on and its version number. A UI editing an existing
draft uses Draft; the first draft edit of a published entity uses Published. Saving with VoxSaveMode.Publish
publishes immediately. If that update was based on a draft, Vox first applies it to the draft and then publishes the
result.
To save into a content release, select both the release perspective and target explicitly:
var releaseScope = VoxContentScope.ForRelease(release.Id);
var edited = await updater.UpdateAsync(
"mug",
new VoxContentVersion(VoxContentMode.Release, product.Version, release.Id),
new ProductActions.SetName { Name = "Launch mug" },
VoxSaveTarget.Release(release.Id),
cancellationToken: cancellationToken);
Read a particular view from an entity store:
var live = await products.GetAsync("mug", VoxContentScope.Published, cancellationToken);
var preview = await products.GetAsync("mug", VoxContentScope.Draft, cancellationToken);
var releasePreview = await products.GetAsync("mug", VoxContentScope.ForRelease(release.Id), cancellationToken);
var versions = await products.GetContentAsync("mug", cancellationToken: cancellationToken);
Publishing and schedules
IVoxEntityPublisher owns the lifecycle operations:
await publisher.PublishAsync<Product>("mug", draftVersion, cancellationToken);
await publisher.ScheduleAsync(productType, "mug", draftVersion, publishAtUtc, cancellationToken);
await publisher.UnscheduleAsync(productType, "mug", draftVersion, cancellationToken);
await publisher.RevertAsync(productType, "mug", draftVersion, cancellationToken);
await publisher.UnpublishAsync(productType, "mug", publishedVersion, cancellationToken);
There is one mutable draft and at most one active schedule. Editing a scheduled draft keeps the schedule, so the latest saved draft is what goes live. Publishing manually invalidates the queued schedule; a delayed duplicate Nexus message is a no-op. Scheduling uses a delayed Nexus queue message. A validation or field-group conflict makes that queue item fail instead of publishing partial or stale content.
Reverting discards pending changes and returns preview reads to the published version. Reverting a draft-only entity
removes it. Unpublishing is the explicit inverse: it removes the live version and retains it as an editable draft.
Deleting remains destructive by default and removes both versions. A caller deleting a draft-only entity passes a
draft VoxContentVersion.
Field-group conflict handling
A draft records the published revision of every field group it changes. If another writer publishes a different field group, Vox rebases that new live value into the preview and the draft remains safe to publish. If both writers changed the same group, publishing returns a conflict and records the conflicting group names on the draft.
Resolve each conflict explicitly with TakeDraft or TakePublished:
await publisher.ResolveAsync(
productType, "mug", draftVersion, "Marketing", VoxConflictResolution.TakeDraft, cancellationToken);
Taking the draft value rebases that group onto the current published revision. Taking the published value removes the group from the draft; if no changed groups remain, the draft is reverted completely.
Drafts may temporarily violate a unique constraint. Vox enforces uniqueness when publishing, because unpublished work must not reserve a live value. Attribute validation and custom validators still run when a draft is saved and again when it is published.
Preview is a complete content scope
Draft and release perspectives are not just ID lookups. Vox maintains layer-aware query fields, property relations, context memberships, calculations, cached projections, entity caches, and search indexes. A preview website can therefore query and navigate the same model it will see after publication.
Entity change subscriptions and read projections consume published changes only. They represent outward-facing delivery and must not leak unfinished work. Imports still update the published groups they own immediately; Vox rebases those changes into any draft unless the draft changed the same group, in which case normal publication conflict handling applies.
Permissions and opting out
Creating and editing a draft uses the entity's normal create/action/edit permissions. Publishing (including an action
saved with VoxSaveMode.Publish), scheduling, reverting, resolving conflicts, and unpublishing additionally require
{EntityType}:Publish. The generic Admin UI exposes this as the Publish permission for each entity type.
Managing releases, including saving an entity version into one, requires vox:ContentReleases. A save also requires
the entity's normal create/action/edit permission. Publishing a release additionally requires
{EntityType}:Publish for every entity type represented in it.
Drafts are enabled by default for owned entities. An operational entity whose changes must always be immediate can opt out:
[VoxDrafts(false)]
public class Cart : IVoxEntity
{
// ...
}
Storage providers declare whether they support the draft lifecycle. Vox rejects a draft-enabled type assigned to a provider without that capability at startup. The built-in Entity Framework provider supports drafts; Redis as an authoritative provider currently does not, so Redis-master types must opt out. Redis can still cache both published and draft Entity Framework reads.
The built-in Entity Framework provider also supports atomic content releases. A provider must explicitly advertise that capability before an entity stored by it can be added to a release; Vox never silently degrades an atomic release into a sequence of independent publications.