Build a starboard

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.

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 ⭐. 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 handle old messages. Discord only sends reaction events for cached messages unless you opt into partials. Partials.Message, Partials.Channel, and Partials.Reaction, plus the .fetch() calls, let the bot react to stars on messages it hasn't seen before.
  • Update, don't duplicate. The 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.
  • 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 MessageReactionRemove listener 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.

Next steps

Was this guide helpful?