Middleware System
How to protect commands with permission checks, rate limits, and custom middleware using kythia-core's pipeline.
Kythia's middleware system provides a clean, declarative way to guard commands before they execute. Instead of writing permission checks inside execute(), you declare guards directly on the command object. The InteractionManager runs them in a pipeline — if any guard fails, execution stops immediately.
The Middleware Pipeline
flowchart TD
A[Discord: InteractionCreate] --> B[InteractionManager\nidentify command]
B --> C{botPermissions\ncheck}
C -- Fail --> ERR1[Reply: bot missing permissions]
C -- Pass --> D{userPermissions\ncheck}
D -- Fail --> ERR2[Reply: user missing permissions]
D -- Pass --> E{ownerOnly\ncheck}
E -- Fail --> ERR3[Silent skip]
E -- Pass --> F{isInMainGuild\ncheck}
F -- Fail --> ERR4[Reply: wrong server]
F -- Pass --> G{cooldown\ncheck}
G -- Fail --> ERR5[Reply: wait X seconds]
G -- Pass --> H[command.execute\ninteraction, container]
style ERR1 fill:#c0392b,color:#fff
style ERR2 fill:#c0392b,color:#fff
style ERR3 fill:#7f8c8d,color:#fff
style ERR4 fill:#c0392b,color:#fff
style ERR5 fill:#e67e22,color:#fff
style H fill:#27ae60,color:#fff
Built-in Guards
All guards are declared as properties on the command module export object.
botPermissions
Ensures the bot itself has the required Discord permissions in the channel where the command is used.
module.exports = {
data: new SlashCommandBuilder().setName('kick'),
botPermissions: ['KickMembers', 'ManageRoles'],
async execute(interaction) { /* ... */ }
};
userPermissions
Ensures the invoking member has the required Discord permissions.
module.exports = {
data: new SlashCommandBuilder().setName('ban'),
userPermissions: ['BanMembers'],
async execute(interaction) { /* ... */ }
};
PermissionFlagsBits string names — the same ones used in PermissionsBitField.ownerOnly
Restricts the command to the single user defined in config.bot.ownerId.
module.exports = {
data: new SlashCommandBuilder().setName('restart'),
ownerOnly: true,
async execute(interaction) { /* ... */ }
};
isInMainGuild / mainGuildOnly
isInMainGuild: true— Restricts execution to the guild defined inconfig.bot.mainGuildId.mainGuildOnly: true— Deploys the command only to the main guild (not globally).
module.exports = {
data: new SlashCommandBuilder().setName('admin-panel'),
isInMainGuild: true,
mainGuildOnly: true,
async execute(interaction) { /* ... */ }
};
cooldown
Per-user cooldown in milliseconds. A user on cooldown gets an ephemeral reply with the remaining wait time.
module.exports = {
data: new SlashCommandBuilder().setName('daily'),
cooldown: 86_400_000, // 24 hours
async execute(interaction) { /* ... */ }
};
Guard Summary
| Property | Type | Description |
|---|---|---|
botPermissions |
string[] |
Permissions the bot must have in the channel |
userPermissions |
string[] |
Permissions the user must have |
ownerOnly |
boolean |
Restrict to config.bot.ownerId |
isInMainGuild |
boolean |
Restrict to config.bot.mainGuildId |
mainGuildOnly |
boolean |
Deploy command to main guild only |
cooldown |
number (ms) |
Per-user cooldown window |
Full Example — Ban Command
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a member.')
.addUserOption(o => o.setName('user').setDescription('User to ban').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Ban reason')),
botPermissions: ['BanMembers'],
userPermissions: ['BanMembers'],
cooldown: 5000, // 5 second anti-spam cooldown
async execute(interaction) {
const target = interaction.options.getUser('user');
const reason = interaction.options.getString('reason') ?? 'No reason provided';
await interaction.guild.members.ban(target, { reason });
await interaction.reply(`Banned **${target.username}** — ${reason}`);
}
};