Only show guides for your app
Run an Express dashboard and your discord.js bot in one Node process — the bot makes outbound connections, the web server takes your one public port. When it's a good idea, when to split into two servers, and the auth you must not skip.
Plenty of bots grow a small web page beside them — live stats, a status endpoint, a control panel. On Falix you can run both in one Node process: the discord.js bot makes only outbound connections, and the Express web server takes your server's single public port. This guide builds that combined process, shares state between the two halves, and is honest about the auth you must add and the point where two separate servers is the better call.
| At a glance | |
|---|---|
| You need | a working discord.js bot and familiarity with Express, on the Node.js application |
| Plan | Free or premium — free runs while the session timer has time |
| Time | about thirty minutes |
Two facts from elsewhere in the guides combine neatly here:
SERVER_PORT is irrelevant to it.SERVER_PORT, and your web server must bind 0.0.0.0 on it.So there's no conflict: the web server claims SERVER_PORT, the bot ignores it, and both live in the same index.js. You get one process to start, one console to watch, and one server to keep online.
Here's the whole thing — the bot, a shared stats object it updates, and an Express app that serves those stats:
require('dotenv').config();
const express = require('express');
const { Client, GatewayIntentBits, Events } = require('discord.js');
// --- the bot ---
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const stats = { ready: false, guilds: 0 };
client.once(Events.ClientReady, (c) => {
stats.ready = true;
stats.guilds = c.guilds.cache.size;
console.log(`Bot connected as ${c.user.tag}`);
});
// --- the web dashboard, same process ---
const app = express();
app.get('/', (req, res) => {
res.json({ ready: stats.ready, guilds: stats.guilds });
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
// Start the bot last; a bad token rejects here without taking the web server down.
client.login(process.env.DISCORD_TOKEN).catch((err) => {
console.error('Bot login failed:', err.code || err.message);
});
The stats object is the bridge: the bot writes to it in ClientReady (and you'd update it wherever else you like), and the web route reads from it. Because they share one process, they share memory — no network call, no database, just a plain object both halves can see.
🎯 Good to know: The
.catchonclient.loginmatters here. If the token is wrong, you don't want the whole process to die and take the web server with it — catching the login failure keeps the dashboard up so you can still see what happened.
The Node.js application flips your server to online when it sees the word Listening in the console — and here that line comes from app.listen's callback: Listening on port …. So the moment the web server is up, the panel shows green, even before the bot finishes connecting. That's fine — the two console.log lines (Listening on port … then Bot connected as …) tell you both halves are alive.
Your dashboard is served on http://your-address:port, and that address is public. Anyone who finds it can hit every route you expose. A read-only stats page is harmless; the moment a route does something — restart the bot, change a setting, read private data — it needs real authentication in front of it.
⚠️ Heads up: Do not put controls behind "nobody knows the URL." They will. Before any route that acts, add proper login. The natural fit for a bot dashboard is Login with Discord — the same accounts your users already have — with a session check guarding each protected route. For the honest minimum on sessions and cookies, see Sessions, JWT, and the honest minimum.
Keep secrets — the bot token, the OAuth client secret — in .env, never in the page or the client-side code. See Environment variables & secrets.
One process is simplest, and for a stats page or a light control panel it's the right choice. Reach for two separate Falix servers — one running the bot, one running the web app — when:
When you split them, they still need to talk — the web app has to read the bot's data. Don't expose that over the public internet: give both servers an Internal Network address and let them talk privately server-to-server, or have both read the same managed database. The database route is often cleanest: the bot writes stats to a table, the dashboard reads them, and neither has to reach into the other's process.
🎯 Good to know: Two servers means two of everything to keep online — on the free plan, two session timers. One combined process is one thing to babysit. Split when a real need above pushes you to, not before.
Start the server. The console shows Listening on port … and then Bot connected as …. Open your server's address (from the Network page) in a browser — you should get the JSON, {"ready":true,"guilds":N}, served by Express while the bot runs alongside it. In Discord, your commands still work. Both jobs, one process, confirmed.
SERVER_PORT and 0.0.0.0, not a hard-coded port or localhost. See I can't reach my app.Listening. Make sure your app.listen callback prints a line containing that word..catch on client.login so a token problem doesn't take the web server down.