A Discord verification bot is a small but vital security layer that stops every new member from reaching your channels until they prove they are a real person. People who pass the gate with a single click or a short captcha code get let in; bot accounts, raid scripts and spam waves stay locked out. In this guide we will build a verification system from scratch with discord.js, starting with a button and hardening it with a captcha. You will set up the role and permission model, automatic role assignment, and the security practices that actually hold up in the real world, step by step.
Why you need a verification system
An open server is a place where anyone who gets hold of the invite link can walk straight in and start typing. That invites two kinds of attack:
- Bot/self-bot floods: Automated accounts accept hundreds of invites in minutes and dump ads or malicious links.
- Raids: Coordinated groups join at once, flood channels, harass members and make the server unusable.
Verification puts that friction exactly where it belongs: a few seconds for a human, but a tedious obstacle for automation. Adding a captcha gives you another tier of protection against advanced bots that mimic simple "click the button" flows.
Setting up the permission and role model
Before writing any code you need the server structure to be right. The logic is simple: an unverified member should see nothing, and access should open once they verify. There are two common patterns:
- Open up with a Verified role: Hide all channels from the
@everyonerole so only a single#verifychannel stays visible. Create a role called "Verified" and grant it View Channel on the real channels. When a member verifies, the bot adds this role and the server opens up. - Restrict with an Unverified role: Automatically give everyone who joins an "Unverified" role, deny it access everywhere, and remove the role on verification.
The first method is usually cleaner because the default state is "closed", reducing the risk of a channel being accidentally left open. Whichever you pick, remember two rules: the bot must have the Manage Roles permission, and the bot's own role must sit above the role it assigns. Otherwise Discord refuses to add the role.
Acting automatically when a member joins
To catch new members we listen for the GuildMemberAdd event. For this event to fire you must enable the bot's Server Members Intent both in the developer portal and in the client options.
const { Client, GatewayIntentBits, Events } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
client.on(Events.GuildMemberAdd, async (member) => {
// If you use the Unverified pattern, assign the role on join:
await member.roles.add(process.env.UNVERIFIED_ROLE_ID);
});
client.login(process.env.TOKEN);
With the Verified pattern this step is unnecessary; the member already sees nothing by default and only the verification channel.
Discord verification bot: verifying with a button
The simplest and fastest method is button-based verification. First you send a persistent panel message to the verification channel:
const { ButtonBuilder, ButtonStyle, ActionRowBuilder } = require('discord.js');
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('verify')
.setLabel('Verify my account')
.setEmoji('✅')
.setStyle(ButtonStyle.Success),
);
await channel.send({
content: 'Click the button below to access the server.',
components: [row],
});
The customId field here is critical: it is the unique identifier that lets you recognise the click. Now let's listen for clicks on this button. We send replies privately with flags: MessageFlags.Ephemeral so the channel doesn't fill up with "you're verified" messages:
const { Events, MessageFlags } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton() || interaction.customId !== 'verify') return;
const role = interaction.guild.roles.cache.get(process.env.VERIFIED_ROLE_ID);
if (interaction.member.roles.cache.has(role.id)) {
return interaction.reply({
content: 'You are already verified.',
flags: MessageFlags.Ephemeral,
});
}
await interaction.member.roles.add(role);
await interaction.reply({
content: 'Verified — welcome aboard!',
flags: MessageFlags.Ephemeral,
});
});
This much is enough for most small communities. But if you want to go one step further against bots that click the button programmatically, that's where the captcha comes in.
Advanced verification with a captcha
In the captcha flow, when a member clicks the button we show them a random code and ask them to type it into a modal form. A simple Map is enough to hold the code in memory (on large servers Redis is preferred). First, open the modal on the button press:
const {
Events,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
} = require('discord.js');
const pending = new Map();
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton() || interaction.customId !== 'verify') return;
const code = Math.random().toString(36).slice(2, 8).toUpperCase();
pending.set(interaction.user.id, code);
const input = new TextInputBuilder()
.setCustomId('captcha_input')
.setLabel(`Type this code exactly: ${code}`)
.setStyle(TextInputStyle.Short)
.setRequired(true);
const modal = new ModalBuilder()
.setCustomId('captcha_modal')
.setTitle('Verification')
.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);
});
When the member submits the form a ModalSubmit interaction arrives. We compare the entered value with the expected code and grant the role if they match:
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isModalSubmit() || interaction.customId !== 'captcha_modal') return;
const expected = pending.get(interaction.user.id);
const given = interaction.fields
.getTextInputValue('captcha_input')
.trim()
.toUpperCase();
if (!expected || given !== expected) {
return interaction.reply({
content: 'Wrong code. Please click the button again.',
flags: MessageFlags.Ephemeral,
});
}
pending.delete(interaction.user.id);
const role = interaction.guild.roles.cache.get(process.env.VERIFIED_ROLE_ID);
await interaction.member.roles.add(role);
await interaction.reply({
content: 'Verified — welcome! 🎉',
flags: MessageFlags.Ephemeral,
});
});
If you want to embed the code in an image instead of plain text, you can render a visual with a library like canvas and send it by DM or an ephemeral message instead of the modal; the logic stays the same, you just make the code harder for machines to read.
Security tips and best practices
- Add a timeout: Clear
pendingentries after a few minutes withsetTimeoutso stale codes don't pile up in memory. - Apply rate limiting: Stop the same user from making too many attempts quickly; auto-kick anyone who keeps failing.
- Keep logs: Write successful and failed verifications to a log channel via webhook; seeing who got through during a raid is invaluable.
- Account-age filter: Auto-kick very recently created accounts (e.g. younger than 7 days) before verification to stop bot waves at the door.
- Protect the token: Never hardcode the bot token; use
process.envwith a.envfile and add it to.gitignore.
Frequently Asked Questions
What permissions does a verification bot need?
The bot needs at least the Manage Roles permission, and the bot's role must sit above the "Verified" role it assigns. To catch member joins, the Server Members Intent must also be enabled.
Captcha or button — which is better?
For small, quiet servers a single-click button is usually enough. On large servers or ones that get raided often, a captcha provides clear extra protection by filtering out bots that auto-click the panel. Combining both is the strongest solution.
Should I store the code in a database instead of a Map?
For small, single-process bots a Map is fast and sufficient. Pending codes are wiped when the bot restarts, which is fine. If you use sharding or need persistence, choose a shared store like Redis.
Need a reliable verification system for your server? I can design and build a custom Discord bot tailored to your community with button, captcha, anti-raid and logging features. Get in touch and let's talk about your project.