A Node.js bridge that connects a NeoForge Minecraft server, a Discord server, and a website into a unified live chat. Messages sent in-game, in Discord, or on the website all appear in all three places in real time.
Built for caaat.dev β a NeoForge 1.21.11 Minecraft server.
β οΈ The mods require NeoForge 1.21.11 and are server-side only - do not install on the client.
| Download | Description |
|---|---|
| π¦ bot.js (source) | The Node.js bridge server |
| π§ caaat_chat_bridge.jar | Chat/events mod β drop into your server's mods/ folder |
| π caaat_stats.jar | Stats mod β drop into your server's mods/ folder |
Full releases and changelogs on the Releases page.
- π¬ Bidirectional chat between Minecraft, Discord, and the website
- π‘ Server-Sent Events (SSE) for real-time updates on the website (no polling)
- π’ Live server status - player count, version, uptime
- π Game events - joins, leaves, deaths, and advancements posted to Discord and website
- π‘οΈ Profanity filter on all website messages
- β±οΈ Rate limiting on website chat (per IP)
- π Bridge controls via Discord slash commands (
/bridge web readonly,/bridge web on) - π WebSocket auth - the Minecraft mod must authenticate with a shared token
βββββββββββββββββββββββββββββββ
β Minecraft Server β
β ββ caaat_chat_bridge ββββββββββββββββββββββββ
β ββ caaat_stats β β
ββββββββββββββ¬βββββββββββββββββ β
β WebSocket /ws β
β (chat, joins, deaths, stats...) β (Discord/web messages
βΌ β relayed to Minecraft)
βββββββββββββββ SSE /events βββββββββββββββββββββββ
β bot.js ββββββββββββββββββΊβ Website (caaat.dev) β
β (Node.js) βββββββββββββββββββ β
ββββββββ¬βββββββ POST / βββββββββββββββββββββββ
β
β Webhook (events)
βΌ
βββββββββββββββ
β Discord β
β #minecraft β
ββββββββ¬βββββββ
β Bot reads messages
ββββββββββββββββββββββββΊ bot.js (loops back above)
- caaat_chat_bridge handles chat, join/leave, death, and advancement events over WebSocket
- caaat_stats sends live player count and server info
- The website sends messages via
POST /and receives real-time updates viaGET /events(SSE) - Discord receives events via a webhook, and the bot watches the channel to relay messages back to Minecraft and the website
- The bot is exposed publicly via a Cloudflare Tunnel (
api.caaat.dev)
- Node.js 18+
- A Discord bot with the following enabled:
Message Content IntentServer Members Intent
- A Discord webhook in your chat channel
- The companion NeoForge mod installed on your Minecraft server
git clone https://github.com/Cooper-Rice/CaaatMinecraftBridge.git
cd CaaatMinecraftBridge
npm installCopy .env.example to .env and fill in your values:
cp .env.example .envDISCORD_TOKEN= # Your Discord bot token
MC_AUTH_TOKEN= # A secret string shared with the Minecraft plugins
WEBHOOK_URL= # Discord webhook URL for your chat channel
CHANNEL_ID= # Discord channel ID to watch for messages
APPLICATION_ID= # Discord application ID (for slash commands)
GUILD_ID= # Your Discord server ID
PORT=3000 # Port to run the HTTP server onnode bot.js| Command | Description | Admin only |
|---|---|---|
/status |
Shows server online status, player count, and uptime | No |
/list |
Lists currently online players | No |
/bridge web on |
Enables website chat fully | Yes |
/bridge web readonly |
Makes website chat read-only | Yes |
| Variable | Description |
|---|---|
DISCORD_TOKEN |
Bot token from Discord Developer Portal |
MC_AUTH_TOKEN |
Shared secret for mod WebSocket authentication |
WEBHOOK_URL |
Full Discord webhook URL |
CHANNEL_ID |
ID of the Discord channel to bridge |
APPLICATION_ID |
Discord app ID (for registering slash commands) |
GUILD_ID |
Discord server ID (for guild-scoped slash commands) |
PORT |
HTTP server port (default: 3000) |
bot.js # Main bridge server
.env # Secrets (never committed)
.env.example # Template for .env
bridge-state.json # Persisted bridge state (auto-generated, never committed)
package.json
Both mods generate a server config file on first launch at:
config/caaat_chat_bridge-server.toml
config/caaat_stats-server.toml
Each config has two values:
# WebSocket URL of your bot.js server
bot_ws_url = "ws://localhost:3000/ws"
# Shared secret token to authenticate with bot.js
bot_token = "changeme"bot_ws_url - where the mod should connect to bot.js. If bot.js is running on the same machine as the Minecraft server, leave this as ws://localhost:3000/ws. If they're on separate machines (e.g. different VMs), replace localhost with the internal IP of the machine running bot.js.
bot_token - a shared secret that must match the MC_AUTH_TOKEN value in your bot.js .env file. Change this to something secret - the mod will be rejected if the tokens don't match.
bot.js exposes two HTTP endpoints that your website connects to. Since bot.js runs locally on your server machine, you'll need a way to expose it publicly - the recommended approach is a Cloudflare Tunnel.
Option A - Cloudflare Tunnel (recommended)
Cloudflare Tunnel lets you expose bot.js to the internet without opening any ports or having a static IP. It's free and works on any machine.
- Install cloudflared
- Authenticate:
cloudflared tunnel login - Create a tunnel:
cloudflared tunnel create my-tunnel - Create a config file at
~/.cloudflared/config.yml:
tunnel: <your-tunnel-id>
credentials-file: /path/to/.cloudflared/<tunnel-id>.json
ingress:
- hostname: api.yourdomain.com
service: http://localhost:3000
- service: http_status:404- Route the tunnel to your domain:
cloudflared tunnel route dns my-tunnel api.yourdomain.com - Run it:
cloudflared tunnel run my-tunnel
Your bot is now reachable at https://api.yourdomain.com.
Option B - Any reverse proxy
You can also expose bot.js via nginx, Caddy, or any other reverse proxy pointed at localhost:3000.
Once bot.js is publicly accessible, your website needs to hit two endpoints:
GET /events - real-time SSE stream
Connect to this on page load to receive live messages, status updates, and player stats:
const events = new EventSource('https://api.yourdomain.com/events');
events.addEventListener('message', (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'history') // array of recent messages on connect
if (msg.type === 'chat') // a chat message (from MC, Discord, or web)
if (msg.type === 'join') // player joined
if (msg.type === 'leave') // player left
if (msg.type === 'death') // player died
if (msg.type === 'advancement') // player got an advancement
if (msg.type === 'stats') // { players, max, version }
if (msg.type === 'status') // { online: bool, readonly: bool }
if (msg.type === 'server') // server started/stopped
});POST / - send a message from the website
await fetch('https://api.yourdomain.com/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'Steve', message: 'Hello from the web!' })
});Messages are rate-limited to one per 1.5 seconds per IP and filtered for profanity. The username field is sanitized to alphanumeric + underscores/hyphens, max 20 characters. Messages are capped at 200 characters.
bridge-state.jsonis auto-generated and saves the current web chat state across restarts- The Minecraft mod connects to
ws://localhost:3000/ws(same machine) or the bot's internal IP if running on separate machines - Slash commands are registered per-guild on startup for instant availability