One of the most critical parts of any bot is the discord bot permission layer that decides who can run which command. Leaving a ban, kick, or server-configuration command open to everyone is a disaster waiting to happen. In this article we'll build a layered permission system with discord.js v14: we'll lean on Discord's own permission model and also design custom flows that require specific roles. The goal is to block unauthorized use not silently, but with clear feedback to the user.
How permissions work in Discord
It helps to keep two distinct concepts apart:
- Permission: Atomic capabilities defined by Discord, such as
BanMembers,ManageGuild, orKickMembers. A member's effective permissions are the union of the permissions granted by all of their roles. - Role: Server-specific groups with a name and a color. Roles like "Moderator" or "VIP" are your own invention; Discord doesn't know them, it only sees the permissions they carry.
A good permission system uses both: the coarse filter comes from Discord's permission flags, while the fine-grained control comes from your own role logic.
First line of defense: setDefaultMemberPermissions
When you define a slash command you can tell Discord directly which permission it requires. As a result, users without that permission won't even see the command in the menu — the cleanest protection, because the check happens on Discord's side.
// commands/moderation/ban.js
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Bans a member from the server')
.addUserOption((o) =>
o.setName('member').setDescription('Member to ban').setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction) {
// ...
},
};
Here setDefaultMemberPermissions only shows the command to roles that hold BanMembers. This is a default starting point; server admins can override it per role or channel from Discord's "Integrations" settings. Even so, don't rely on it alone — you must validate inside your code too.
Validating the permission in code
On top of Discord's UI filter, re-checking the caller's permission server-side while the command runs is the foundation of security. interaction.memberPermissions returns a PermissionsBitField, which you query with .has().
async execute(interaction) {
if (!interaction.memberPermissions.has(PermissionFlagsBits.BanMembers)) {
return interaction.reply({
content: 'You need the **Ban Members** permission to use this command.',
ephemeral: true,
});
}
// permission granted, continue processing
}
Thanks to ephemeral: true, only the person who ran the command sees the warning; the channel doesn't fill up with messages from unauthorized attempts.
Flows that require a role
Sometimes a command depends not on a built-in Discord permission but on a role you defined — for example, only people with the "Support Team" role should be able to close a ticket. You check a member's roles via interaction.member.roles.cache. Querying a role by ID is far more reliable than by name, because roles can be renamed.
const SUPPORT_ROLE_ID = '123456789012345678';
async execute(interaction) {
const allowed = interaction.member.roles.cache.has(SUPPORT_ROLE_ID);
if (!allowed) {
return interaction.reply({
content: 'Only the Support Team can perform this action.',
ephemeral: true,
});
}
// ticket-closing flow...
}
If you want to accept any one of several roles, put the allowed IDs in an array and check with some:
const ALLOWED_ROLES = ['111...', '222...'];
const allowed = ALLOWED_ROLES.some((id) => interaction.member.roles.cache.has(id));
Avoid repetition: a reusable permission layer
Copying the same if blocks into every command is error-prone. A cleaner approach is to add a permission contract to the command file and perform the check in the central interactionCreate event. The command merely declares its requirement; the handler enforces it.
// commands/moderation/clear.js
module.exports = {
data: /* ...SlashCommandBuilder... */,
permissions: [PermissionFlagsBits.ManageMessages], // required permissions
roles: ['MOD_ROLE_ID'], // required roles (optional)
async execute(interaction) { /* ... */ },
};
// inside events/interactionCreate.js
const command = interaction.client.commands.get(interaction.commandName);
if (!command) return;
// permission check
if (command.permissions?.length) {
const missing = command.permissions.filter(
(p) => !interaction.memberPermissions.has(p));
if (missing.length) {
return interaction.reply({
content: 'You lack the permissions required for this command.',
ephemeral: true,
});
}
}
// role check
if (command.roles?.length) {
const ok = command.roles.some(
(id) => interaction.member.roles.cache.has(id));
if (!ok) {
return interaction.reply({
content: 'You are not allowed to use this command.',
ephemeral: true,
});
}
}
await command.execute(interaction);
Now, to add a new protected command all you do is write a permissions or roles field on the file; the repeated checking code lives in a single place.
Don't forget the bot's own permissions
A common mistake: even when the user is authorized, the bot itself may lack permission to perform the action. Before banning, check the role hierarchy and the bot's permissions, otherwise the API returns an error.
const target = interaction.options.getMember('member');
const me = interaction.guild.members.me;
if (!me.permissions.has(PermissionFlagsBits.BanMembers)) {
return interaction.reply({
content: 'I lack the Ban Members permission. Please check the bot’s role permissions.',
ephemeral: true,
});
}
if (target && target.roles.highest.position >= me.roles.highest.position) {
return interaction.reply({
content: 'This member has a role higher than mine, so I cannot act on them.',
ephemeral: true,
});
}
In Discord, a member can only act on members whose highest role is below their own. The same rule applies to the bot, so remember to move the bot's role high enough in the role list.
Frequently Asked Questions
Isn't setDefaultMemberPermissions enough — why also check in code?
Because setDefaultMemberPermissions only affects UI visibility and can be overridden by server admins. The real security decision must be made in code, on the server side. The two layers together provide both a clean user experience and reliable protection.
Should I check a role by name or by ID?
Always by ID. Role names can change and several roles may share the same name; the ID is unique and permanent. You can copy the ID by right-clicking the role with Developer Mode enabled in Discord.
Can I separate the permission configuration from the code?
Yes. As you scale, storing role IDs and command permissions in a database or a config file is a good idea. That way each server can define its own roles, and you don't have to deploy code just to change who has access.
Is your bot's permission system leaking, or do you want to build one from scratch? We can put permission and role checks, the bot's permission hierarchy, and a central authorization layer on a solid footing together. Get in touch and let's make your bot secure.