Adds slash commands, services, migrations, and member event handlers for configuring per-guild auto-roles and role replacement rules. Also updates shared error/permission messaging to support the new role management flows.
Everything Bot
Modular Discord bot built with Node.js, Discord.js, and SQLite. The first feature is reaction roles, with an architecture designed so new commands and events can be added without rewriting the core.
Requirements
- Node.js 20 or newer (Node 22 recommended)
- A Discord application and bot user
- npm
Features
- Guild-only slash commands for creating, listing, removing, and clearing reaction roles
- Unicode, static custom, and animated custom emoji support
- Multiple mappings per message, shared roles across messages, multi-guild storage
- Persistent SQLite storage that survives restarts
- Automatic command and event loading from folders
- Graceful shutdown and structured logging (secrets never logged)
Installation
npm install
cp .env.example .env
Edit .env with your Discord credentials (see below).
Discord application setup
- Open the Discord Developer Portal.
- Click New Application, name it, and create it.
- Open the Bot tab and click Add Bot if needed.
- Under Token, click Reset Token / Copy and save it for
DISCORD_TOKEN. - Open the OAuth2 → General tab and copy the Application ID for
DISCORD_CLIENT_ID.
Required gateway intents
In the Bot tab, enable:
| Intent | Required | Notes |
|---|---|---|
| Server Members Intent | Yes | Needed to fetch members and assign/remove roles |
| Message Content Intent | No | Not required for reaction roles |
The bot also requests these gateway intents in code:
- Guilds
- Guild Members
- Guild Message Reactions
Required bot permissions
When inviting the bot, grant at least:
View Channels
Read Message History
Add Reactions
Manage Roles
Use Application Commands
Role hierarchy: the bot’s highest role must sit above every role it needs to assign. In Server Settings → Roles, drag the bot’s role above those roles.
Invite the bot
- Open OAuth2 → URL Generator.
- Scopes:
botandapplications.commands. - Bot permissions: select the permissions listed above (or use permission integer
268438560as a starting point — verify in the UI). - Open the generated URL, choose your server, and authorize.
Environment variables
Copy .env.example to .env:
DISCORD_TOKEN=
DISCORD_CLIENT_ID=
DISCORD_GUILD_ID=
DATABASE_PATH=./data/bot.sqlite
| Variable | Required | Description |
|---|---|---|
DISCORD_TOKEN |
Yes | Bot token |
DISCORD_CLIENT_ID |
Yes | Application ID |
DISCORD_GUILD_ID |
No | Development server ID for fast command updates |
DATABASE_PATH |
No | SQLite file path (default ./data/bot.sqlite) |
The bot validates required variables on startup and exits with a clear error if any are missing.
Database
SQLite is initialized automatically on startup. The data/ directory and database file are created if missing. No manual migration step is required.
The reaction_roles table stores:
id,guild_id,channel_id,message_id,role_idemoji_identifier,emoji_displaycreated_by,created_at
A uniqueness constraint on (guild_id, message_id, emoji_identifier) prevents duplicate emoji mappings on the same message.
Slash command deployment
npm run deploy-commands
- With
DISCORD_GUILD_IDset: commands are registered to that guild and usually appear within a few seconds. Prefer this while developing. - Without
DISCORD_GUILD_ID: commands are registered globally and may take up to about an hour to propagate to all servers.
Re-run deployment whenever you add or change slash command definitions.
Running the bot
Development (restarts on file changes):
npm run dev
Production:
npm start
Lint:
npm run lint
Reaction-role commands
All of these require Manage Roles and are guild-only. Responses are ephemeral.
Create
/reaction-role message_id:<message ID> role:<Discord role> emoji:<emoji>
Example:
/reaction-role message_id:123456789012345678 role:@Game Updates emoji:🎮
The command:
- Checks Manage Roles for the user
- Finds the message in the current channel
- Validates the role and bot hierarchy / permissions
- Rejects integration-managed roles
- Validates the emoji and adds the bot’s reaction
- Saves the mapping in SQLite
- Confirms with message, role, and emoji
List
/reaction-role-list
Shows all mappings for the current server in an embed (channel, message ID, emoji, role), with pagination when needed.
Remove one mapping
/reaction-role-remove message_id:<message ID> emoji:<emoji>
Deletes the mapping and attempts to remove only the bot’s reaction.
Clear a message
/reaction-role-clear message_id:<message ID>
Deletes every mapping for that message and attempts to remove the bot’s configured reactions.
Custom emoji syntax
| Type | Example | Stored identifier |
|---|---|---|
| Unicode | 🎮 |
The character itself |
| Static custom | <:gaming:123456789012345678> |
Custom emoji ID |
| Animated custom | <a:dance:123456789012345678> |
Custom emoji ID |
To insert custom emoji markup in the command: in Discord, type \:emojiName: in chat to reveal the raw form, then paste it into the emoji option.
Custom emojis must be available to the bot (from a server the bot shares). Matching always uses the stored identifier so unicode and custom emojis stay consistent across restarts.
Discord role hierarchy
Discord only allows a bot to assign roles strictly below its highest role. If assignment fails:
- Open Server Settings → Roles
- Move the bot’s role above the target role
- Confirm the bot still has Manage Roles
- Confirm the target role is not managed by an integration (boosts, bots, Linked Roles, etc.)
The bot also refuses to assign the @everyone role and roles managed by integrations.
Project structure
src/
commands/
reactionRoles/
createReactionRole.js
listReactionRoles.js
removeReactionRole.js
clearReactionRoles.js
events/
interactionCreate.js
messageReactionAdd.js
messageReactionRemove.js
ready.js
services/
reactionRoleService.js
database/
database.js
migrations.js
utilities/
emoji.js
permissions.js
logger.js
errors.js
loadCommands.js
loadEvents.js
reactionRoleHandler.js
config/
environment.js
deployCommands.js
index.js
Responsibilities are split across commands, events, services, database, utilities, and config so features stay isolated.
Adding future commands
- Create a new file under
src/commands/(any subfolder). - Export a default object:
import { SlashCommandBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong'),
async execute(interaction) {
await interaction.reply('Pong!');
},
};
- Run
npm run deploy-commands. - Restart the bot if it is already running.
Commands are discovered automatically — no central switch statement to edit.
Adding future event handlers
- Create a file under
src/events/. - Export a default object:
import { Events } from 'discord.js';
export default {
name: Events.GuildCreate,
once: false,
async execute(guild) {
// ...
},
};
Set once: true for one-time events such as ClientReady. Events are registered automatically on startup.
Troubleshooting
| Problem | What to try |
|---|---|
| Commands do not appear | Run npm run deploy-commands. For global commands, wait up to an hour, or set DISCORD_GUILD_ID for instant guild deploy. |
| “Message not found” | Use a message in the same channel where you run the command. Enable Developer Mode to copy the message ID. |
invalid ELF header / better-sqlite3 crash |
node_modules was installed on a different OS (e.g. Windows) then copied to Linux. On the server, run rm -rf node_modules && npm install. Do not copy node_modules between machines. |
| Roles are not assigned | Enable Server Members Intent. Ensure the bot role is above the target role and has Manage Roles. |
| Custom emoji rejected | The bot must be in a server that has that emoji. Paste full <:name:id> / <a:name:id> markup. |
| Duplicate mapping error | That emoji is already configured on the message — remove it first or pick another emoji. |
| Bot cannot react | Grant Add Reactions, View Channels, and Read Message History in that channel. |
| Database errors | Ensure the process can write to DATABASE_PATH (default ./data/). |
| Login fails | Check DISCORD_TOKEN in .env (no quotes/spaces). Reset the token in the Developer Portal if needed. |
Graceful shutdown
The bot handles SIGINT, SIGTERM, unhandled rejections, and uncaught exceptions. On shutdown it stops accepting new work, destroys the Discord client, closes SQLite, and logs completion.
License
MIT