Skip to Content
Documentation

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

plugins/site-notes/src/staff-permissions.ts
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"] },
});
PropertyValue
adminReferences to the admin permissions: staffPermissions.admin.notes.canCreate
moderatorReferences to the moderator permissions: staffPermissions.moderator.notes.canDelete
configThe same declarations as plain data. buildApiPlugin reads it; you do not pass it yourself
pluginIdThe 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.

AcceptsType
buildRoute({ adminStaffPermission })AdminStaffPermissionRef
buildRoute({ moderatorStaffPermission })ModeratorStaffPermissionRef
AdminCP nav items and sub-items, dashboard widgetsAdminStaffPermissionRef

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:

plugins/newsroom/src/api/modules/admin/stories/routes/feature.route.ts
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"),
  // ...
});
HelperFor
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-permissionsAny 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.