Discord Bot Dashboard: Building a Web Panel for Non-Technical Admins in 2026

Published on | Updated on

A web dashboard lets server admins configure your Discord bot without touching code or config files. Here's how to build one using Express and Discord OAuth2.

Written by Jochem, Infrastructure Engineer at Space-Node, 5-10 years experience in game server hosting, VPS infrastructure, and 24/7 streaming solutions. Read author bio →

Your bot works perfectly. But every time a server admin wants to change the welcome message, update the auto-role, or see bot statistics, they have to come to you. A web dashboard puts configuration in admins' hands without requiring them to touch configuration files.

Architecture

Bot (discord.js) ─── Shared database ─── Dashboard (Express.js + Discord OAuth2)
                                                │
                                          Admin browser

Both the bot and the dashboard read/write the same database. When an admin changes a setting in the dashboard, the bot reads the updated setting on next use.

Discord OAuth2 Authentication

Admins log in via Discord - no separate account needed:

const express = require('express');
const session = require('express-session');
const passport = require('passport');
const { Strategy: DiscordStrategy } = require('passport-discord');

passport.use(new DiscordStrategy({
    clientID: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
    callbackURL: process.env.CALLBACK_URL,
    scope: ['identify', 'guilds']
}, async (accessToken, refreshToken, profile, done) => {
    // Store user profile in session
    return done(null, profile);
}));

app.get('/auth/discord', passport.authenticate('discord'));
app.get('/auth/callback', passport.authenticate('discord', {
    successRedirect: '/dashboard',
    failureRedirect: '/'
}));

// Middleware: only show dashboard to server admins
function requireGuildAdmin(req, res, next) {
    const guild = req.user.guilds.find(g => g.id === req.params.guildId);
    if (!guild || !(guild.permissions & 0x8)) {  // 0x8 = ADMINISTRATOR
        return res.status(403).send('Access denied');
    }
    next();
}

Guild Settings Panel

app.get('/dashboard/:guildId', requireGuildAdmin, async (req, res) => {
    const settings = await db.query('SELECT * FROM guild_settings WHERE guild_id = $1', [req.params.guildId]);
    res.render('dashboard', { settings: settings.rows[0] });
});

app.post('/dashboard/:guildId/save', requireGuildAdmin, async (req, res) => {
    const { welcomeMessage, autoRoleId, logChannelId } = req.body;
    
    await db.query(
        `INSERT INTO guild_settings (guild_id, welcome_message, auto_role_id, log_channel_id)
         VALUES ($1, $2, $3, $4)
         ON CONFLICT (guild_id) DO UPDATE SET
         welcome_message = $2, auto_role_id = $3, log_channel_id = $4`,
        [req.params.guildId, welcomeMessage, autoRoleId, logChannelId]
    );
    
    res.redirect(`/dashboard/${req.params.guildId}?saved=true`);
});

The bot then reads from guild_settings table instead of hardcoded values.

Host your bot dashboard on Space-Node

Why most Discord bots eventually grow a dashboard

Slash commands are great until your config has more than 5 toggles. A dashboard wins for: per-guild settings, role/channel pickers, audit logs, and customer support without giving everyone admin commands.

Architecture that works

Browser  ->  Web (Next.js / SvelteKit)  ->  API (REST or tRPC)  ->  Bot process
                       |                              |
                       +---- OAuth2 (Discord) --------+
                       +---- Postgres (config) -------+
                       +---- Redis (cache/session) ---+

Don't run the bot inside the web process. The bot needs a long-lived gateway connection; web servers cycle on deploy. Two processes, one DB.

Discord OAuth2 scopes you actually need

ScopeRequired for
identifylogin
guildslist user's servers
guilds.members.readcheck user's roles in a guild

Do NOT request bot or applications.commands from the dashboard login. Those belong to the bot's own invite link.

Permission check pattern

A user is allowed to edit a guild's bot config only if:

  1. The bot is in that guild (cross-check your own DB).
  2. The user has MANAGE_GUILD permission in that guild.
const MANAGE_GUILD = 0x20;
const canManage = (BigInt(g.permissions) & BigInt(MANAGE_GUILD)) === BigInt(MANAGE_GUILD);

Never trust client-side filtering. Re-check on the API route.

Real-world trap: rate limits

Discord rate-limits the /users/@me/guilds endpoint at ~5 requests / 5 seconds per user. Cache the user's guild list in Redis for 60-300 s; otherwise a refresh-spamming user will get your bot rate-limited globally.

Making config changes take effect immediately

The web saves to Postgres. The bot caches config. Either:

  • Bot re-reads on every command (simple, fine for low load).
  • Use Redis pub/sub: web publishes config:update:{guildId}, bot subscribes and invalidates its cache.

The pub/sub pattern is what lets the dashboard show "Saved" and have the bot honor the change in under a second.

Jochem

About the Author

Jochem, Infrastructure Engineer at Space-Node, expert in game server hosting, VPS infrastructure, and 24/7 streaming solutions with 5-10 years experience.

Since 2023
500+ servers hosted
4.8/5 avg rating

I specialize in Minecraft, FiveM, Rust, and 24/7 streaming infrastructure, operating enterprise-grade AMD Ryzen 9 hardware in Netherlands datacenters.

View my full bio and credentials →

Keep Your Bot Online 24/7

Reliable Discord bot hosting powered by enterprise AMD Ryzen 9 hardware. Start free, upgrade anytime with guaranteed uptime.