RocketChat/Rocket.Chat · error · Meteor.Error
error-role-protected
error-role-protected
Error message
Cannot delete a protected role
What it means
Thrown by POST /api/v1/roles.delete when the target role has its `protected` flag set. Rocket.Chat marks built-in roles (admin, user, bot, moderator, owner, leader, livechat-agent, and similar system roles) as protected because core authorization logic depends on them; the delete flow refuses to remove them regardless of permissions.
Solutions
- Don't delete built-in roles — filter them out of any automated cleanup (check the protected field from roles.list)
- To stop a built-in role being assignable, manage its permissions instead of deleting the role
- If you need a variant, create a custom role rather than repurposing a protected one
Example fix
// before
await sdk.post('roles.delete', { roleId });
// after (never attempt protected roles)
const role = roles.find((r) => r._id === roleId || r.name === roleId);
if (role?.protected) throw new Error(`role ${role.name} is protected and cannot be deleted`);
await sdk.post('roles.delete', { roleId }); Defensive patterns
Strategy: validation
Validate before calling
const { roles } = await sdk.get('roles.list');
const target = roles.find((r) => r._id === roleId || r.name === roleId);
if (target?.protected) {
throw new Error(`role '${target.name}' is protected — manage its permissions instead of deleting it`);
}
await sdk.post('roles.delete', { roleId: target?._id ?? roleId }); Type guard
const isProtectedRole = (role: { protected?: boolean } | undefined): boolean =>
role?.protected === true; Try / catch
catch 'error-role-protected' and skip permanently — the guard is intentional and no retry or permission change will bypass it; adjust your cleanup list to exclude built-in roles.
Prevention
- Filter roles.list results by !protected before offering deletion in UIs/scripts
- Treat built-in roles as configuration to adjust (permissions), not resources to delete
- Document which roles ship protected so operators don't try
When it happens
Trigger: POST /api/v1/roles.delete { roleId: <id-or-name> } targeting any built-in role — e.g. 'admin', 'moderator', 'livechat-agent'. The role resolves fine, but role.protected is true in the database, so the guard fires before the in-use check.
Common situations: Cleanup scripts trying to 'reset' a workspace by deleting default roles; admins attempting to hide a built-in role by deleting it; migrations that assume all roles are deletable.
Related errors
- error-invalid-role
- error-not-allowed
- error-role-in-use
- error-action-not-allowed
- error-action-not-allowed
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/8454985b0ec9460c.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/roles.ts:243
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { bodyParams } = this;
const role = await Roles.findOneByIdOrName(bodyParams.roleId);
if (!role) {
throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
}
if (role.protected) {
throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role');
}
if ((await Roles.countUsersInRole(role._id)) > 0) {
throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use");
}
await Roles.removeById(role._id);
void notifyOnRoleChanged(role, 'removed');
return API.v1.success();
},
)
.post(
'roles.removeUserFromRole',
{
authRequired: true,
permissionsRequired: ['access-permissions'],View on GitHub (pinned to b2c16d5842)