Only show guides for your app
Greet everyone who joins with an embed in your welcome channel — and turn on the one privileged intent that makes join events reach your bot at all.
This recipe posts a welcome message every time someone joins your server: a friendly embed with the new member's name, avatar, and their member number. The catch — and the whole reason this needs its own guide — is that join events don't reach your bot until you flip one privileged switch in the Developer Portal.
| At a glance | |
|---|---|
| You need | a working bot from Host a discord.js bot, and a channel to post welcomes in |
| Plan | Free or premium |
| Time | About fifteen minutes |
Start from the discord.js bot guide if you haven't — you need a bot with your token in .env already running.
Your bot can't be told about new members unless Discord is allowed to send it member events, and that permission is a privileged intent you toggle by hand.
⚠️ Heads up — this is a privileged intent. In the Discord Developer Portal, open your application → Bot → scroll to Privileged Gateway Intents → turn on Server Members Intent → save. The code below requests
GatewayIntentBits.GuildMembers; if that toggle is off, the bot crashes at login withUsed disallowed intentsinstead of starting. On servers over 100 members Discord also asks you to verify the bot before it will grant this intent.
The complete index.js. It reads the target channel's ID from .env, so you don't hard-code it:
require('dotenv').config();
const {
Client, Events, GatewayIntentBits,
EmbedBuilder,
} = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
],
});
client.once(Events.ClientReady, (c) => {
console.log(`Listening as ${c.user.tag}`);
});
client.on(Events.GuildMemberAdd, async (member) => {
const channel = member.guild.channels.cache.get(process.env.WELCOME_CHANNEL_ID);
if (!channel) return;
const embed = new EmbedBuilder()
.setColor(0x57f287)
.setTitle('👋 Welcome!')
.setDescription(`Hey ${member}, welcome to **${member.guild.name}**!`)
.setThumbnail(member.user.displayAvatarURL())
.addFields(
{ name: 'Member', value: `${member.user.tag}`, inline: true },
{ name: 'You are member #', value: `${member.guild.memberCount}`, inline: true },
)
.setTimestamp();
await channel.send({ content: `${member}`, embeds: [embed] });
});
client.login(process.env.DISCORD_TOKEN);
Then add the channel's ID to your .env, next to the token:
DISCORD_TOKEN=your-token
WELCOME_CHANNEL_ID=123456789012345678
To get a channel's ID, enable Developer Mode in Discord (User Settings → Advanced), then right-click the welcome channel and choose Copy Channel ID. Save .env and restart.
GatewayIntentBits.GuildMembers in the intents array is what asks Discord to send GuildMemberAdd events. It only works because you enabled the matching toggle above — the two must agree.Events.GuildMemberAdd fires once per join. Its member argument is the person who joined, carrying their guild (member.guild) and user (member.user).member.guild.channels.cache.get(process.env.WELCOME_CHANNEL_ID) looks up the channel to post in. The if (!channel) return guard means a wrong or deleted ID fails quietly instead of crashing.${member} in a string becomes a live @mention that pings the new member. Putting it in content (outside the embed) is what actually notifies them — mentions inside an embed don't ping.member.guild.memberCount is the running total, so "You are member #1,024" comes for free.The embed itself is standard — Embeds deep dive covers every field it can hold.
member.guild.systemChannel to use whatever channel Discord already flags for system messages — no ID needed.await member.send('Welcome!') messages them directly. Wrap it in try/catch: people who block DMs from server members will throw an error.Events.GuildMemberRemove handler for a leave message — it uses the same member object and needs the same intent.Used disallowed intents at startup — the Server Members Intent toggle is off in the Developer Portal. Turn it on (see the callout) and restart. See Bot appears offline.WELCOME_CHANNEL_ID is wrong. Confirm the ID with Copy Channel ID and that the bot can see and post in that channel.content, not inside the embed. The code puts ${member} in content for exactly this reason.