Users
Permission references
How VitNode's typed staff permission references work: defineStaffPermissions naming, admin and moderator refs, other plugins and Content Engine permissions.
defineStaffPermissions(manifest, { admin, moderator }) from @vitnode/core/lib/staff-permissions declares a plugin's staff permissions and returns a typed reference for each one. Route guards, AdminCP navigation items and dashboard widgets accept only these references, so a typo, an undeclared permission or a moderator permission on an admin route is a compile error. Permissions walks through declaring and granting one; this page is the lookup for how references behave.
What defineStaffPermissions returns
import { defineStaffPermissions } from "@vitnode/core/lib/staff-permissions";
import { manifest } from "@/manifest.gen";
export const staffPermissions = defineStaffPermissions(manifest, {
admin: {
notes: ["can_view", { permission: "can_create", dependsOn: ["can_view"] }],
},
moderator: { notes: ["can_delete"] },
});| Property | Value |
|---|---|
admin | References to the admin permissions: staffPermissions.admin.notes.canCreate |
moderator | References to the moderator permissions: staffPermissions.moderator.notes.canDelete |
config | The same declarations as plain data. buildApiPlugin reads it; you do not pass it yourself |
pluginId | The plugin id from the manifest |
Pass the whole result to buildApiPlugin({ permissionStaff: staffPermissions }). Entries are a permission name, or { permission, dependsOn?, scope? } for dependencies and moderator scopes.
Keep the call in src/staff-permissions.ts. It is browser-safe, so src/admin/nav.tsx can import it, and keeping it out of config.api.ts avoids a circular import between routes and the plugin config. vitnode generate reads and edits it there.
Naming rule
A reference's key is the permission name in camelCase: - and _ separate words. can_create becomes canCreate, view-logs becomes viewLogs.
Two different names in one module of one staff type that would share a key, such as can_edit and can-edit, make defineStaffPermissions throw when the file loads:
defineStaffPermissions: admin permissions in module "notes" collide on reference key "canEdit": "can_edit" and "can-edit" both become admin.notes.canEdit. Rename or remove one of them.Listing the same name twice, or the same name under admin and moderator, is fine.
Admin and moderator references
A reference carries its staff type. At runtime it is a frozen { staffType, plugin, module, permission } object, and its type is AdminStaffPermissionRef or ModeratorStaffPermissionRef.
| Accepts | Type |
|---|---|
buildRoute({ adminStaffPermission }) | AdminStaffPermissionRef |
buildRoute({ moderatorStaffPermission }) | ModeratorStaffPermissionRef |
AdminCP nav items and sub-items, dashboard widgets | AdminStaffPermissionRef |
The distinction is enforced at compile time: staffPermissions.moderator.notes.canDelete in adminStaffPermission does not compile, even when admin declares a permission with the same name. A hand-written { module: "notes", permission: "can_create" } is not a reference and does not compile either. buildRoute also refuses the wrong staff type at startup, for JavaScript callers, and vitnode doctor reports it.
Another plugin's permissions
A reference names the plugin that declares it, and a guard checks that plugin's permission, not the route's own. To guard a route with core's Edit users permission, import core's declarations:
import { coreStaffPermissions } from "@vitnode/core/lib/core-staff-permissions";
buildRoute({
adminStaffPermission: coreStaffPermissions.admin.users.canEdit,
// ...
});Content Engine permissions
A content type or container collection generates its permissions at runtime (can_view, can_create, can_edit, can_delete, plus can_publish, can_hide or can_restore when it enables those features), so they are not in your defineStaffPermissions call. Reference them with the helpers from @vitnode/core/content:
import { contentAdminStaffPermission } from "@vitnode/core/content";
import { buildRoute } from "@/api/builders";
import { storyContentType } from "@/content/story";
import { manifest } from "@/manifest.gen";
export const featureStoryRoute = buildRoute({
adminStaffPermission: contentAdminStaffPermission(manifest.id, storyContentType, "can_edit"),
// ...
});| Helper | For |
|---|---|
contentAdminStaffPermission(pluginId, contentType, permission) | A content type's generated admin permission |
contentContainerAdminStaffPermission(pluginId, containers, permission) | A container collection's admin permission |
declaredStaffPermission({ entries, module, permission, plugin, staffType }) from @vitnode/core/lib/staff-permissions | Any other permission catalog built at runtime |
These are the dynamic catalog boundary. They are checked when the module loads, not by the compiler: each one throws at startup when the content type does not generate the permission, for example can_publish on a content type without publication. pluginId must be the plugin that registers the content type.
Use a reference in a check
checkStaffPermission, assertStaffPermission and stored grants work with plain { plugin, module, permission } data. staffPermissionArgs(ref) converts a reference:
import { checkStaffPermission } from "@vitnode/core/api/lib/check-staff-permission";
import { staffPermissionArgs } from "@vitnode/core/lib/staff-permissions";
const canCreate = await checkStaffPermission(c, {
type: "admin",
...staffPermissionArgs(staffPermissions.admin.notes.canCreate),
});Front-end checks such as useAdminPermission({ plugin, module, permission }) and editable pages' permission still take plain objects, so they are not checked against the declarations at compile time.
What a reference does not check
A guard decides who may call a route. It does not decide which record they may change: an administrator with Edit notes passes the guard on every note. Check ownership, a content type's access rules or a moderator's scope in the handler or service when that matters.