Documentation

Publishing policies reference

Reference for Content Engine container publishing policies - the defineContentContainer policies option, the five policy keys and their fallbacks, the resolution order, the GET and PUT policies routes, the policy report, errors and server helpers.

Exact names, values and behavior of container publishing policies. For a guided setup, follow Publishing policies. Examples use the Forum plugin's forum.forums collection.

policies option

defineContentContainer({ policies }) from @vitnode/core/content:

plugins/forum/src/content/forums.ts
policies: {
  defaults: { publish: "immediate", edits: "immediate", authorEdit: "always" },
  contentTypes: {
    "forum.comment": { authorEdit: "window", authorEditMinutes: 30 },
  },
},
NameTypeRequiredDefaultDescription
defaultspolicyNo{}Applies to every content type filed in the collection
contentTypesRecord<contentTypeId, policy>No{}Per content type, such as "forum.comment". Wins over defaults

Omit policies, or set it to false, and the collection does not take part: its content types use publicWrite.create.status, owners edit for as long as update.own allows, can_edit stays unscoped and no can_approve exists. policies: {} turns the feature on with every key on its fallback.

Policy keys

A policy is a partial object of these keys. Each one is resolved on its own.

KeyValuesFallbackDescription
create"members" | "moderators""members"Who may create. "moderators" means can_edit holders in the container
publish"immediate" | "approve"From publicWrite.create.status: "published" → "immediate", "draft" → "approve"Whether a new record goes live or waits as a draft with a submission
edits"immediate" | "approve""immediate"Whether an author's edit of a published record applies or becomes a proposal
authorEdit"always" | "never" | "window""always"How long the owner may edit
authorEditMinutesWhole number from 1 to 525600 (a year)60The window for "window", counted from the record's server createdAt

The constants are exported from @vitnode/core/content: CONTENT_CONTAINER_POLICY_KEYS, CONTENT_CONTAINER_POLICY_CREATE, CONTENT_CONTAINER_POLICY_MODES, CONTENT_CONTAINER_POLICY_AUTHOR_EDIT and CONTENT_CONTAINER_POLICY_MAX_MINUTES.

Resolution order

For each key, the first layer that sets it wins:

  1. the record's container's override for the content type;
  2. the nearest ancestor's override, walking parentId;
  3. policies.contentTypes[contentTypeId];
  4. policies.defaults;
  5. the fallback in the table above.

A missing key inherits. An explicit value, including "never" or a value equal to the parent's, is an override and stops the walk. The engine never stores undefined, and empty per-type objects are dropped on save.

The record's container is always resolved on the server: from its field.container(), or for a field.relation({ container: true }), from the related record's container. A comment follows its topic, whatever the payload claims.

Who passes which rule

CheckPasses
Create with "moderators"can_edit for the container, a root role
Skip publish / edits approvalcan_approve for the container, a root role
Edit any recordcan_edit for the container, a root role. Independent of authorEdit
Edit own recordThe owner, while update.own is on, the record is not hidden and authorEdit allows it

Container scopes are explained in Container-scoped permissions.

Refusals on the public write routes

Both answer 403 with "code": "CONTENT_FORBIDDEN" and one issue whose field is null:

Issue codeConstantWhen
edit_window_closedCONTENT_PUBLIC_WRITE_ISSUE_CODES.editWindowClosedThe owner edits a public record after authorEdit stopped allowing it
moderators_onlyCONTENT_PUBLIC_WRITE_ISSUE_CODES.moderatorsOnlyA create in a container with create: "moderators" by a non-moderator
PATCH /api/@vitnode/forum/content/comments/18 → 403
{
  "code": "CONTENT_FORBIDDEN",
  "contentTypeId": "forum.comment",
  "issues": [
    { "code": "edit_window_closed", "field": null, "message": "Public write denied: forbidden." }
  ]
}

An owner editing a record the public cannot read, such as a hidden one, still gets 404 with no issue, so the answer never confirms it exists. A submission opened by publish or edits is described in Approvals.

AdminCP routes

Mounted only for a collection with policies, in the admin module, under /api/{pluginId}/admin/content/{admin.permissionModule}. For the forum that is /api/@vitnode/forum/admin/content/forum_forums.

Method and pathAdmin permissionBodySuccess
GET /{id}/policiescan_view200 with the report
PUT /{id}/policiescan_edit{ overrides }200 with the new report

PUT replaces the container's whole overrides object. overrides maps a content type id to a policy, and only content types filed in the collection with publicWrite are accepted. A saved change expires the collection's page cache and emits content.container.updated with changedFields: ["policies"]. Saving the same overrides again changes nothing and emits nothing.

PUT /api/@vitnode/forum/admin/content/forum_forums/3/policies
{ "overrides": { "forum.topic": { "publish": "approve" } } }

Policy report

NameTypeDescription
idnumberThe container
overridesRecord<contentTypeId, policy>What this container sets itself
contentTypes[].contentTypeIdstringA content type filed in the collection with publicWrite
contentTypes[].pluginIdstringIts plugin
contentTypes[].effectivefull policyWhat applies in this container
contentTypes[].inheritedfull policyWhat would apply without this container's own overrides
contentTypes[].sourcesRecord<key, source>Where each effective value comes from

A source is { kind: "container", containerId }, { kind: "contentType" }, { kind: "default" } or { kind: "fallback" }.

Errors

Errors answer { code, message }:

StatuscodeWhen
400invalidInputThe body breaks the schema: an unknown key, a value outside the enum, minutes out of range
400invalidPolicyoverrides names a content type that is not filed in the collection
403Missing the admin permission
404notFoundNo such container in this collection

Definition errors

defineContentContainer throws a ContentEngineError when the module loads:

CauseMessage starts with
policies is not an object or falsepolicies is an object with `defaults` and `contentTypes`, or `false`.
An unknown key beside defaults and contentTypespolicies.x is not an option.
A contentTypes key that is not plugin.entitypolicies.contentTypes has the key "x", which is not a content type id
An unknown policy keypolicies.defaults.x is not a policy.
A value outside the allowed onespolicies.defaults.publish is not valid:

Server helpers

From @vitnode/core/content (browser-safe):

FunctionReturnsBehavior
resolveContentContainerPolicy({ chain, contentTypeId, fallback, policies }){ policy, sources }The pure resolution above. chain is the container, then its ancestors, nearest first
contentContainerPolicyFallback(definition)ContentContainerPolicyThe fallback for one content type
contentAuthorMayEdit(policy, createdAt, now?)booleanWhether the owner may still edit

From @vitnode/core/content/server:

FunctionReturnsBehavior
resolveContentRowContainer(c, db, definition, row)ContentRecordContainer | nullThe container an existing row is filed in, with its ancestor chain
resolveContentWriteContainer(c, db, definition, values)ContentRecordContainer | nullThe container a new record would be filed in
effectiveContentPolicy(definition, container)EffectiveContentContainerPolicy | nullnull when the collection has no policies
resolveContentPublicationPolicy({ actor, c, container, definition, operation: "create", pluginId }){ approval, policy, status }How a member's new record is published. Throws moderators_only when creating is not allowed
canModerateContent(c, { container, definition, permission, pluginId })Promise<boolean>Checks a moderator permission against the container's scope

The container service from createContentContainerService has policies(id) and updatePolicies(id, overrides), the same calls the routes make.