Skip to content

Roles and Features

Settings → Roles is an Admin-editable array. Rank is drag-to-reorder. Ticks are the catalogue rows released on Features, plus the Theme grant. Developer is implicit — every catalogue feature, including ones not released, and Theme. That row is visible only to a Developer, with every tick on and locked.

Defaults ship in the package (the Theme grant on Designer; Designer also has Users and Roles; Admin + Designer both store). An Admin may remove Admin, Designer, or Author after first run; only Developer stays. A site may pass extras into createRoles — each extra is a slug, a display label, and default grants — and an Admin can add further custom rows and edit any row’s name. Remaining seed slugs stay read-only.

canManageBrand and the other predicates read the saved Global and fall back to that seed (plus extras). The package never writes developer onto a user row.

Removing a row removes that slug from every user in the same save, so a later row saved under the same slug cannot hand it back. A user whose only role it was is left with none. A save that would leave no user holding Developer or a role with the Users tick is refused (ROLES_LOCKOUT_MESSAGE), whether by deleting the row or unticking Users. pruneUserRoles(req) strips every slug not on the saved Global; call it once from a migration to clean users saved before this rule.

createRoles({
extras: [{ role: "editor", label: "Editor", grants: ["content"] }],
});

Settings → Features is a Developer-only release valve: one switch per catalogue row. Off hides the tick on Roles; Developer still has the feature. The catalogue is code, grouped for the UI: Pages (Content, Publish), Media, and Users (Users, Roles). Theme is required and is not a row. Features does not install or remove the Theme Global. Features itself is not a row.

The document API tab and Better Editor overlay settings are not rows either — they stay code-locked to Developer (adminOnlyApiTab, developerOnlyAccess / hideUnlessDeveloper).

A site may pass extras into createFeatures — those rows appear only there, including the Navigation tick createNavigation reads:

createFeatures({
extras: [
{ slug: "doctors", label: "Doctor search", group: "users" },
{ slug: "navigation", label: "Navigation" },
],
});

canUseFeature(slug) is true when the row is released and a held role is granted it, or when the login is Developer. canUseFeature("theme") is the Roles grant only: Features cannot withhold it. An empty Global falls back to the catalogue defaults. The group slug pages is true when any child is.

createPages / createMedia use canUseFeature for create and update (and admin.hidden). createTheme and createBrandAssets default update to canUseFeature("theme"). That one grant opens the Theme document and brand-assets. A site may pass access.read to tighten read. admin.hidden is presentation; Access Control is enforcement. hideUnlessFeature("theme") hides Theme in the nav for a role with no grant. getPublishedTheme and themeHead still return the saved document. Nav hiding follows user.allowedFeatures on the JWT (computed at user read from the live Features and Roles Globals).

Seed both Globals in the same migration as Theme. Pass the same extras into seedRoles / seedFeatures as into the factories.