---
title: "Discord suggestion system with voting, approval and a thread | StreetHosting"
description: "Build a suggestion system with discord.js v14: /suggest, button votes without double voting, approval with a reason, a color changing embed and a thread."
url: "https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot"
type: "page"
language: "en-US"
---

Discord Bots · 7 min · Intermediate

Published on Nov 13, 2026 · Updated on Nov 13, 2026

# Discord suggestion system: /suggest, votes, staff approval and a thread

An organized suggestions channel: the person uses /suggest, the bot posts an embed with vote buttons, opens a thread for the conversation and staff approve or reject with a reason, changing the embed color.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Automation and webhooks](https://streethosting.com.br/en/guides/topics/automation)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=Discord%20suggestion%20system%20with%20voting%2C%20approval%20and%20a%20thread&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot "Share on LinkedIn") [](https://wa.me/?text=Discord%20suggestion%20system%20with%20voting%2C%20approval%20and%20a%20thread%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-suggestion-system-bot "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot.md)

In this guide 8 sections

* [How the system works](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#como-funciona)
* [Channel, permissions and commands](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#preparar)
* [The /suggest command](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#enviar)
* [Button votes with no double voting](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#votos)
* [Approve or reject with a reason](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#avaliar)
* [Alternative: voting with reactions](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#reacoes)
* [Care to avoid losing data](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#cuidados)
* [Where to host the bot](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#onde-rodar)

Quick answer

The `/suggest` command posts an embed in the suggestions channel with two vote buttons and opens a thread on the message for discussion. The bot stores, for each suggestion, the list of who voted on each side, which prevents double voting. Staff use `/suggestion approve` or `/suggestion reject` with a reason, and the bot edits the embed, changing the color and disabling the buttons.

## How the system works[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#como-funciona)

Loose suggestions in a chat channel get lost in minutes. The system we are going to build gives each idea a place, a vote count and an official answer. The flow has four steps:

1. The person uses `/suggest` and writes the idea.
2. The bot publishes a numbered embed in the suggestions channel, with Agree and Disagree buttons, and opens a thread on that message.
3. Members vote with the buttons and debate in the thread. Each person has one vote, which can be switched.
4. Staff approve or reject with a reason. The embed changes color, shows the answer and the buttons are disabled.

All of this uses only slash commands, buttons and embeds, so the `Guilds` intent is enough. If any of these concepts is new, the guides on [buttons and interactive menus](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus) and on [embeds](https://streethosting.com.br/en/guides/discord-bots/discordjs-embed-formatted-messages) explain the basics.

## Channel, permissions and commands[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#preparar)

Create a text channel just for suggestions and copy its ID into the `SUGGESTIONS_CHANNEL_ID` environment variable. A tip for organization: remove the send messages permission from the `@everyone` role in that channel. Since only the bot posts, the channel stays clean, and anyone who wants to weigh in uses the thread.

| Bot permission in the channel  | What it is for                                         |
| ------------------------------ | ------------------------------------------------------ |
| View Channel and Send Messages | Publish the suggestion embed                           |
| Embed Links                    | Show the embed                                         |
| Create Public Threads          | Open the thread on the message                         |
| Send Messages in Threads       | Announce in the thread when the suggestion is reviewed |

Now the definition of the two commands. The first is open to everyone. The second, for reviewing, is born restricted to people with the Manage Server permission. Registering the commands with the API is covered in the guide on [slash commands](https://streethosting.com.br/en/guides/discord-bots/create-discord-bot-slash-commands).

`const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); const suggestCmd = new SlashCommandBuilder() .setName('suggest') .setDescription('Send a suggestion to the server') .addStringOption((o) => o.setName('text').setDescription('Your suggestion').setRequired(true).setMaxLength(1000) ); const review = (name, description) => (sub) => sub .setName(name) .setDescription(description) .addIntegerOption((o) => o.setName('number').setDescription('Suggestion number').setRequired(true).setMinValue(1) ) .addStringOption((o) => o.setName('reason').setDescription('Reason for the decision').setRequired(true).setMaxLength(500) ); const suggestionCmd = new SlashCommandBuilder() .setName('suggestion') .setDescription('Review suggestions (staff)') .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand(review('approve', 'Approve a suggestion')) .addSubcommand(review('reject', 'Reject a suggestion'));`

## The /suggest command[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#enviar)

First the base: the client, the file storage and the function that draws the buttons. Each suggestion lives in an object with number, author, text, status and the two lists of voters. The `suggestions.json` file is rewritten on every change, which keeps the system going after restarts.

`const fs = require('node:fs'); const { Client, GatewayIntentBits, Events, MessageFlags, EmbedBuilder, Colors, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits, } = require('discord.js'); const CHANNEL_ID = process.env.SUGGESTIONS_CHANNEL_ID; const FILE = './suggestions.json'; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); // items is indexed by the ID of the suggestion's message let data = { next: 1, items: {} }; try { data = JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch { // first run, start empty } const save = () => fs.writeFileSync(FILE, JSON.stringify(data)); function buttons(s, disabled = false) { return new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('vote:yes') .setLabel('Agree (' + s.yes.length + ')') .setEmoji('👍') .setStyle(ButtonStyle.Success) .setDisabled(disabled), new ButtonBuilder() .setCustomId('vote:no') .setLabel('Disagree (' + s.no.length + ')') .setEmoji('👎') .setStyle(ButtonStyle.Danger) .setDisabled(disabled), ); }`

And now the command handler. It builds the embed, sends it to the suggestions channel, saves the data and opens the thread:

`async function suggest(interaction) { const channel = await interaction.client.channels.fetch(CHANNEL_ID).catch(() => null); if (!channel) { return interaction.reply({ content: 'The suggestions channel is not configured.', flags: MessageFlags.Ephemeral, }); } const text = interaction.options.getString('text'); const number = data.next++; const s = { number, authorId: interaction.user.id, status: 'open', yes: [], no: [], }; const embed = new EmbedBuilder() .setColor(Colors.Gold) .setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL(), }) .setTitle('Suggestion #' + number) .setDescription(text) .setFooter({ text: 'Voting open' }) .setTimestamp(); const message = await channel.send({ embeds: [embed], components: [buttons(s)] }); s.messageId = message.id; data.items[message.id] = s; save(); await message .startThread({ name: ('Suggestion #' + number + ': ' + text).replace(/\s+/g, ' ').slice(0, 100), autoArchiveDuration: 1440, }) .catch((error) => console.error('Could not open the thread:', error.message)); await interaction.reply({ content: 'Your suggestion was posted in ' + channel.toString() + '.', flags: MessageFlags.Ephemeral, }); }`

The reply to the author is ephemeral, meaning only they see it, and that avoids cluttering the channel where the command was used. The `autoArchiveDuration` of 1440 minutes archives the thread after a day with no activity. The accepted values are 60, 1440, 4320 and 10080, and the thread reopens by itself when someone writes in it.

## Button votes with no double voting[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#votos)

The trick against double voting is to store who voted, not only how many voted. On each click the bot checks the suggestion's two lists and applies the rule: at most one vote per person, switching sides allowed and a repeated click taking the vote back.

`async function vote(interaction) { const s = data.items[interaction.message.id]; if (!s || s.status !== 'open') { return interaction.reply({ content: 'This suggestion is already closed.', flags: MessageFlags.Ephemeral, }); } const id = interaction.user.id; if (id === s.authorId) { return interaction.reply({ content: 'You cannot vote on your own suggestion.', flags: MessageFlags.Ephemeral, }); } const side = interaction.customId === 'vote:yes' ? 'yes' : 'no'; const other = side === 'yes' ? 'no' : 'yes'; // remove the vote from the opposite side, if there is one s[other] = s[other].filter((x) => x !== id); // clicking the same side again takes the vote back if (s[side].includes(id)) { s[side] = s[side].filter((x) => x !== id); } else { s[side].push(id); } save(); await interaction.update({ components: [buttons(s)] }); }`

The `interaction.update` edits the message where the click happened, refreshing the numbers on the buttons without sending a new message. Blocking votes on your own suggestion is optional, but it is usually worth it: without it the author can pull the count in their favor.

One vote per account doesn't stop someone who creates extra accounts. If your server is a target for that, require a minimum account age or time in the server before allowing votes. Discord offers no way to guarantee that each human has only one account.

## Approve or reject with a reason[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#avaliar)

Staff use `/suggestion approve` or `/suggestion reject` giving the suggestion number and the reason. The bot fetches the message, copies the embed with `EmbedBuilder.from`, changes the color, adds a field with the answer and disables the buttons.

`async function decide(interaction) { // check the permission in code too, even with setDefaultMemberPermissions if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { return interaction.reply({ content: 'Only staff can review suggestions.', flags: MessageFlags.Ephemeral, }); } const approve = interaction.options.getSubcommand() === 'approve'; const number = interaction.options.getInteger('number'); const reason = interaction.options.getString('reason'); const s = Object.values(data.items).find((x) => x.number === number); if (!s || s.status !== 'open') { return interaction.reply({ content: 'I could not find an open suggestion with number #' + number + '.', flags: MessageFlags.Ephemeral, }); } const channel = await interaction.client.channels.fetch(CHANNEL_ID); const message = await channel.messages.fetch(s.messageId).catch(() => null); if (!message) { return interaction.reply({ content: 'The message for this suggestion was deleted.', flags: MessageFlags.Ephemeral, }); } s.status = approve ? 'approved' : 'rejected'; save(); const embed = EmbedBuilder.from(message.embeds[0]) .setColor(approve ? Colors.Green : Colors.Red) .setFooter({ text: approve ? 'Approved' : 'Rejected' }) .addFields({ name: 'Staff answer (' + interaction.user.username + ')', value: reason, }); await message.edit({ embeds: [embed], components: [buttons(s, true)] }); // the thread has the same ID as the message. Fetch it from the API because // an archived thread leaves the cache and message.thread comes back null const thread = await channel.threads.fetch(message.id).catch(() => null); if (thread) { await thread .send('Suggestion ' + (approve ? 'approved' : 'rejected') + ': ' + reason) .catch(() => {}); } await interaction.reply({ content: 'Suggestion #' + number + (approve ? ' approved.' : ' rejected.'), flags: MessageFlags.Ephemeral, }); }`

Finally, the router that connects everything to the `interactionCreate` event. The buttons are recognized by the prefix of the `customId`:

`client.on(Events.InteractionCreate, async (interaction) => { try { if (interaction.isChatInputCommand()) { if (interaction.commandName === 'suggest') await suggest(interaction); if (interaction.commandName === 'suggestion') await decide(interaction); } else if (interaction.isButton() && interaction.customId.startsWith('vote:')) { await vote(interaction); } } catch (error) { console.error('Error in the suggestion system:', error); } }); client.login(process.env.DISCORD_TOKEN);`

For staff, the quickest way to find the number is to look at the embed title. If you prefer a flow with a form, the guide on [modals and forms](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms) shows how to open a text window in place of the command option.

## Alternative: voting with reactions[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#reacoes)

You can swap the buttons for reactions, which many communities prefer for the look. The bot sends the embed and reacts with 👍 and 👎 for members to click. The cost is losing fine control: Discord's count includes the bot's own vote, which has to be discounted, and each person can react with both emojis. To prevent that, remove the opposite reaction in code. You need the `GuildMessageReactions` intent, the message, channel and reaction partials (to receive events from old messages) and the Manage Messages permission for the bot:

`const OPPOSITE = { '👍': '👎', '👎': '👍' }; client.on(Events.MessageReactionAdd, async (reaction, user) => { if (user.bot) return; if (reaction.partial) await reaction.fetch().catch(() => null); if (reaction.message.channelId !== CHANNEL_ID) return; // old message: fetch it to get the other reactions into the cache if (reaction.message.partial) await reaction.message.fetch().catch(() => null); const opposite = OPPOSITE[reaction.emoji.name]; if (!opposite) return; const other = reaction.message.reactions.cache.get(opposite); if (other) await other.users.remove(user.id).catch(() => {}); });`

Even so, for a suggestions channel that needs reliable numbers, buttons are the safer choice. If the idea is just a quick yes or no vote, with no approval flow, see the guide on [polls and voting](https://streethosting.com.br/en/guides/discord-bots/discord-bot-polls-voting).

## Care to avoid losing data[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#cuidados)

* **Don't use collectors for the buttons.** A collector only exists while the process is alive. By handling the click directly in `interactionCreate`, the buttons on old suggestions keep working after a deploy or a crash.
* **Persist everything that matters.** The JSON file is enough for a small server. With many suggestions or more than one server, use a database. The bot plan includes a MariaDB database, and if you prefer MongoDB, the guide on [connecting the bot to a MongoDB database](https://streethosting.com.br/en/guides/discord-bots/connect-discord-bot-to-database) shows the way.
* **Back up the file.** Without a backup, a failing disk takes the whole vote history with it.
* **Limit abuse.** A minimum interval between suggestions from the same user prevents channel spam, and the 1000 character limit on the command keeps everything readable.

## Where to host the bot[](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#onde-rodar)

A suggestion system is light, but it only works with the bot online: with it down, nobody can vote or send ideas. On [StreetHosting bot hosting](https://streethosting.com.br/en/bots) the Discord Essential plan, at R$ 9.99 per month, with 1 GB of RAM, 0.5 vCPU, 2 GB of NVMe and up to 2 bots, runs this Node.js project with room to spare, all controlled from the container panel. To keep the process up all the time, see [Discord bot hosting 24/7](https://streethosting.com.br/en/guides/discord-bots/discord-bot-hosting-24-7).

In this guide

* [How the system works](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#como-funciona)
* [Channel, permissions and commands](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#preparar)
* [The /suggest command](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#enviar)
* [Button votes with no double voting](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#votos)
* [Approve or reject with a reason](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#avaliar)
* [Alternative: voting with reactions](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#reacoes)
* [Care to avoid losing data](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#cuidados)
* [Where to host the bot](https://streethosting.com.br/en/guides/discord-bots/discord-suggestion-system-bot#onde-rodar)

## Frequently asked questions

How do I stop the same person from voting twice?

For each suggestion, store the list of IDs of who voted on each side. When a click arrives, remove the ID from the opposite side, and if it was already on the clicked side, treat it as taking the vote back. That way each person has at most one vote and can still change their mind.

Do the buttons keep working after the bot restarts?

They do, as long as you handle the click in the interactionCreate event using the customId and the message ID, instead of using a collector. A collector lives in memory and dies with the process. And the vote data has to be in a file or database, not only in a variable.

Who can approve or reject suggestions?

Whoever you decide. The two common paths are requiring the Manage Server permission, with setDefaultMemberPermissions on the command and a check inside the code, or checking that the person has a staff role whose ID sits in the bot's configuration.

Voting with buttons or with reactions: which is better?

Buttons give an exact count, prevent double voting reliably and can be disabled when closing. Reactions are simpler and more visual, but they count the bot's own vote and allow voting on both sides, unless you remove the opposite reaction in code.

What is the maximum size of a suggestion?

An embed description takes up to 4096 characters, but for suggestions it is worth limiting to 1000 with setMaxLength on the command option, which keeps the channel readable and avoids giant texts.

Next step

See bot plans

24/7 Discord bots with fast activation and support in Portuguese.

[See bot plans](https://streethosting.com.br/en/bots)

## Related guides

[Discord bots Intermediate Discord bot polls: native poll, reactions or buttons Discord has a native poll and the bot can send it with one field on the message. Learn when it is enough, when reactions or buttons make more sense, how to limit to one vote and how to read the result. 7 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discord-bot-polls-voting) [Discord bots Intermediate Buttons and select menus in a Discord bot with discord.js v14 Learn to build ButtonBuilder and StringSelectMenuBuilder, read the customId in interactionCreate, meet the 3 second deadline, choose between a collector and a global handler and disable used buttons. 7 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus) [Discord bots Beginner Discord embeds with discord.js: how to build formatted messages The embed is the colored card that gives your bot's messages a professional look. Learn to build one from scratch, the official limits, what markdown works inside it and how to edit an embed that was already sent. 7 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discordjs-embed-formatted-messages)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
