Only show guides for your app
When a message gets enough ⭐ reactions, repost it to a highlights channel and keep the star count updated — with the intents it needs and an honest note about avoiding duplicates after a restart.
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.
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.)
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.
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.
MessageReactionAdd checks the emoji, the threshold (reaction.count), and that the message isn't from a bot. reaction.count is the total number of that reaction, so the embed's footer always shows the current star tally.Partials.Message, Partials.Channel, and Partials.Reaction, plus the .fetch() calls, let the bot react to stars on messages it hasn't seen before.posted map 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.🎯 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.
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.
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.
| 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. |