Permissions
Permissions answer: may this role perform this operation on this model?
They do not answer: which rows? That is scope.
Ada and Grace are both ADMIN. They have the same permissions. They do not see the same posts.
Per-model allowlists
admin.register("Post", {
permissions: {
list: ["SUPER_ADMIN", "ADMIN"],
view: ["SUPER_ADMIN", "ADMIN"],
create: ["SUPER_ADMIN", "ADMIN"],
update: ["SUPER_ADMIN", "ADMIN"],
delete: ["SUPER_ADMIN"],
actions: {
publish_selected: ["SUPER_ADMIN", "ADMIN"],
},
},
});Operations: list, view, create, update, delete, plus named actions.
The four rules people miss
- Production configuration — every model must declare
permissionswhenNODE_ENV=production; development keeps the authenticated-read default for local setup. - Omitted writes —
create,update,delete, and custom actions are denied unless you explicitly allow a role. - Empty list —
delete: []denies everyone exceptisSuperAdmin. - Super-admin —
isSuperAdmin: truebypasses these lists. It does not bypassscope().
// Development only: anyone signed in can read User, but nobody except a super-admin can write it.
admin.register("User");
// Allow ADMIN to create and update, but reserve deletion for SUPER_ADMIN.
admin.register("User", {
permissions: {
create: ["ADMIN"],
update: ["ADMIN"],
delete: ["SUPER_ADMIN"],
},
});Be explicit about every write operation you want enabled. Partial objects deny omitted writes.
Protecting individual fields
Use writeRoles to protect sensitive application fields even when a role may update the model:
admin.register("User", {
permissions: { update: ["SUPER_ADMIN", "ADMIN"] },
fields: {
role: { writeRoles: ["SUPER_ADMIN"] },
isActive: { writeRoles: ["SUPER_ADMIN"] },
},
});The field remains visible in lists but is excluded from edit forms for other roles. The API enforces this too. role, isActive, and isSuperAdmin additionally default to super-admin-only writes unless you explicitly configure writeRoles.
How the UI uses this
GET /admin/api/schema returns booleans for the current person:
{
"permissions": {
"list": true,
"view": true,
"create": true,
"update": true,
"delete": false,
"actions": { "publish_selected": true }
}
}The sidebar hides models without list. The built-in Delete selected list action is hidden without delete. The API still enforces the same check — hiding a control is not the security boundary.
Custom actions
An action needs list permission and the action allowlist (allowedRoles on the action and/or permissions.actions[name]). Missing records in the selection abort the whole action. See Custom actions.