A starboard is a server hall-of-fame: react to a great message with ⭐, and once it passes a threshold the bot reposts it to a dedicated highlights channel, keeping the star count fresh as more people pile on. This recipe builds one in discord.js, covering the reaction intents and the partials that make it work on older messages.
| At a glance | |
|---|---|
| You need | A working bot from Host a discord.js bot, and a channel to use as the starboard |
| Plan | Free or premium — no premium features required |
| Time | About twenty-five minutes |
New here? Build the base bot with Host a discord.js bot first.
Turn on the Message Content intent
A starboard reposts the text (and images) of other people's messages, which requires the Message Content Intent — a privileged intent. On the Discord Developer Portal → your app → Bot, switch it on. (Reactions themselves are not privileged, so that's the only toggle you need.)
Set the starboard channel
Put the highlights channel's ID in your .env so it stays out of the code:
STARBOARD_CHANNEL_ID=123456789012345678
Turn on Developer Mode in Discord (Settings → Advanced), then right-click the channel → Copy Channel ID.
The whole bot
Here's the complete index.js. A message needs three ⭐ to make the board (change THRESHOLD).
require('dotenv').config();
const { Client, GatewayIntentBits, Events, Partials, EmbedBuilder } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
});
const STARBOARD_CHANNEL_ID = process.env.STARBOARD_CHANNEL_ID;
const THRESHOLD = 3;
const STAR = '⭐';
// original message id -> starboard message id. In memory: resets on restart.
const posted = new Map();
client.once(Events.ClientReady, (c) => console.log(`Listening as ${c.user.tag}`));
client.on(Events.MessageReactionAdd, async (reaction) => {
if (reaction.partial) await reaction.fetch().catch(() => {});
if (reaction.emoji.name !== STAR) return;
const message = reaction.message.partial ? await reaction.message.fetch().catch(() => null) : reaction.message;
if (!message || message.author?.bot) return;
if (reaction.count < THRESHOLD) return;
const board = await client.channels.fetch(STARBOARD_CHANNEL_ID).catch(() => null);
if (!board) return;
const embed = new EmbedBuilder()
.setColor(0xffac33)
.setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
.setDescription(message.content || '*no text*')
.addFields({ name: 'Source', value: `[Jump to message](${message.url})` })
.setFooter({ text: `${STAR} ${reaction.count}` })
.setTimestamp(message.createdAt);
const image = message.attachments.find((a) => a.contentType?.startsWith('image/'));
if (image) embed.setImage(image.url);
const existingId = posted.get(message.id);
if (existingId) {
const boardMsg = await board.messages.fetch(existingId).catch(() => null);
if (boardMsg) {
await boardMsg.edit({ embeds: [embed] });
return;
}
}
const sent = await board.send({ embeds: [embed] });
posted.set(message.id, sent.id);
});
client.login(process.env.DISCORD_TOKEN);
Restart, react to a message with ⭐ from three accounts, and watch it appear in your starboard channel.
How it works
- The reaction event fires on every ⭐.
MessageReactionAddchecks the emoji, the threshold (reaction.count), and that the message isn't from a bot.reaction.countis the total number of that reaction, so the embed's footer always shows the current star tally. - Partials handle old messages. Discord only sends reaction events for cached messages unless you opt into partials.
Partials.Message,Partials.Channel, andPartials.Reaction, plus the.fetch()calls, let the bot react to stars on messages it hasn't seen before. - Update, don't duplicate. The
postedmap remembers which starboard post belongs to which original message. If the message is already on the board, the bot edits the existing post (bumping the count) instead of posting it again. - Images come along — the first image attachment is pulled into the embed.
🎯 Good to know: Want stars to drop off the board when people un-react? Add a
MessageReactionRemovelistener that re-checks the count and edits or deletes the board post. It's the mirror image of the code above.
What resets on a restart
The posted map — the memory of which message maps to which board post — lives in RAM, so a restart forgets it. After a restart, a message that's already on the board could get posted a second time when someone stars it again. For a busy server, persist that mapping: a tiny SQLite table of original_message_id → board_message_id, loaded on boot, closes the gap. See Storing data for your bot; the giveaway bot shows the same load-on-boot pattern.
Verify it works
React to a message with ⭐ from fewer than three accounts and confirm nothing happens. Cross the threshold and confirm the message appears in the starboard channel with the author, text, a jump link, and ⭐ 3. Add another star and confirm the count on the existing board post ticks up rather than a new post appearing.
Troubleshooting
| Symptom | Fix |
|---|---|
Starboard posts show *no text* |
The Message Content intent is off, or the message is older than the bot's cache. Turn the intent on. |
| Nothing posts at all | Check STARBOARD_CHANNEL_ID is set and the bot can send messages there; confirm you crossed THRESHOLD. |
| A message posts twice | Expected after a restart — the posted map was cleared. Persist it to prevent duplicates. |
Used disallowed intents |
Enable Message Content in the Developer Portal Bot tab. |