Discord Bot Dashboard: Building a Web Panel for Non-Technical Admins in 2026
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.