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

  1. Don't delete built-in roles — filter them out of any automated cleanup (check the protected field from roles.list)
  2. To stop a built-in role being assignable, manage its permissions instead of deleting the role
  3. 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

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


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)