← all projects

Rival Drafts

Production-grade Marvel Rivals Discord bot + infrastructure: matchmaking, tournaments, moderation, API auto-verify, two-VPS failover.

live
Node.js 20discord.js v14SQLitebetter-sqlite3Dockerdocker-composesystemdrsyncMarvel Rivals APITwitch API

Overview

Rival Drafts is a production Discord bot I built for a Marvel Rivals PUG community. It runs 12-player matchmaking with captain drafts and per-queue ELO, full tournament brackets (single/double elim, Swiss, round robin), moderation + automod, ticket-based rank verification, Marvel Rivals API auto-confirmation, Twitch go-live notifications, daily username verification, an admin web dashboard, daily cross-VPS backups, and an active-passive failover system with automatic failback. Deployed on two VPSs via Docker Compose, operated under systemd, monitored via webhook alerts.

Highlights

  • Shipped in ~10 days: 130+ commits, 60+ slash commands across 5 surfaces.
  • Two-VPS infrastructure: primary + standby with health-check watchdog, auto-failover, and auto-failback.
  • Daily cross-VPS database backups over rsync/SSH, 7-day retention, Discord webhook alerts on success/failure.
  • Marvel Rivals API integration: auto-verify match results + daily username sync (handles renames, DMs players).
  • Interactive tournament dashboard V2: single/double elim, Swiss, round robin, hidden competitive seeding, live bracket updates.
  • Interactive automod panel: spam / invites / caps / bad words / mass mentions, configured via buttons + modals.
  • Seven-round systematic audit pass: 40+ targeted fixes, race-condition guards, memory-leak patching.

Context

A Marvel Rivals community I was part of ran PUG nights manually — spreadsheets for ELO, mod calls in DMs, tournament brackets drawn on paper. Saturday setup was an hour before first match. I built one bot that would eat the whole workflow: queue, draft, match, report, verify, moderate, season, tournament. Then I kept going until it felt like infrastructure, not a script.

By the numbers

  • ~10 days, first commit to production (Apr 11 → Apr 21, 2026).
  • 130+ commits — feature work, refactors, and seven explicit audit rounds.
  • 60+ slash commands: 14 matchmaking, 9 moderation, 28+ admin, 13 tournament subcommands, 1 ticket-panel.
  • 10+ SQLite tables — players, per-queue stats, matches, pending reports, bans, hero pool, seasons, tournament, queue state, Twitch links, config.
  • 2 VPSs, 2 Docker containers (bot + admin dashboard), 1 systemd service, 1 watchdog, 1 backup job.
  • 3 third-party integrations: Marvel Rivals API, Twitch API, Discord webhooks.

Infrastructure

Designed to survive a VPS outage during a live tournament. Primary runs the bot and the dashboard. Standby receives daily DB backups and can take over automatically.

  • Primary VPS: rivals-bot-bot (Node.js + discord.js + health endpoint on :3000) and rival-drafts-dashboard (web admin UI on :80), both in Docker.
  • Standby VPS: backup storage at /opt/rivals-backup, watchdog script, ready-to-start copy of the bot.
  • Health-check watchdog: 30s poll, 3 fails → failover, 5 successes → failback. Discord webhook alert on every transition.
  • Daily rsync backup at 04:00 UTC — both SQLite DBs plus .env, docker-compose.yml, package.json. 7-day rolling archive.
  • Docker image: node:20-alpine, runs as non-root 'node' user via entrypoint.sh, persistent data volume.
  • Locked internal health endpoint, bind bot port to 127.0.0.1 only (reverse-proxy terrain).

Engineering highlights

The parts that moved it from "Discord bot" to "system."

  • Active-passive failover with watchdog + DB sync + webhook alerts — primary down, standby takes over; primary back, failback.
  • Marvel Rivals API auto-verify: after a report, poll the MR custom-room API (5 min initial delay, 3 min interval), cross-reference team rosters, auto-confirm including MVP/SVP, flag mismatches between reported and API results. 15-min manual timer skipped on API match.
  • Daily username verification job: 24h cron-like sweep over all linked MR names, rate-limited (2s delay). 404 from the API → username cleared + DM to re-register.
  • Tournament V2 dashboard: single interactive message with dropdown nav, rank-based hidden seeding, live bracket updates, format + BO switching without recreating the tournament.
  • Interactive automod config panel: staff tune spam / invite / caps / mass-mention / bad-word policies via buttons + modals, zero config-file editing.
  • Multi-queue architecture: Open (all ranks) + Pro (Rank X/A only), fully separate ELO, stats, channels, and persistence.
  • Queue persistence + timer recovery: queue state saved on every join/leave, restored on bot restart with timers recalculated from elapsed time.
  • Twitch integration: polls Twitch API for linked players, posts go-live notifications to a configured channel.
  • Admin web dashboard: separate container, live bot stats, quick-action buttons.

Quality process

Seven explicit audit rounds after the initial feature push. Each round had a theme and a commit listing every fix.

  • Round 5: ELO baseline, dodge cooldown, balance-teams safety, ticket role errors.
  • Round 6: 11 fixes — format vote, ban queue, timer auth, queueId integrity, ELO falsy-check, match-ID collision, streak limits.
  • Round 7: moderation client param, timer recovery, setelo scope, ready-check cleanup, tournament timer stop.
  • Plus: "17 audit fixes" stability/security/reliability sweep, memory-leak hunt, polish pass for deprecation warnings + race conditions.

Architecture

discord.js v14 codebase with commands grouped by concern. Persistence consolidated on SQLite via better-sqlite3 after a mid-build migration from JSON (dead JSON backup module removed once stable). One big matchdb.js (~600 lines) owns match/player/ELO; a lean bot.db handles warnings + tickets.

  • src/index.js — startup, command + event loading, recovery, periodic jobs, health endpoint, graceful shutdown.
  • src/commands/ — matchmaking, moderation, admin, tickets, tournament (5 surfaces).
  • src/events/ — ready, interactionCreate router, guildMemberAdd/Remove.
  • src/handlers/ — buttons.js (400+ lines), tickets/ (creation, close, rank verification), tournament-v2.js (bracket logic).
  • src/matchmaking.js + queue.js + elo.js + state.js — core PUG flow and rating math.
  • src/rivals-api.js — Marvel Rivals API client (register verification, match verification, daily sweep).
  • src/twitch-monitor.js + twitch.js — stream polling + API helpers.
  • src/automod.js — filter + interactive config.
  • src/utils/ — embed builders, action logger — logger writes every mod action / ELO change / admin action / ticket event to a log channel.
  • Dockerfile + docker-compose.yml + entrypoint.sh — one-command deploy as non-root 'node' user.

Ops & operations

  • Deploy: ssh into primary → git pull → docker compose down + build --no-cache + up -d → deploy-commands.js if slash commands changed.
  • Live logs via docker compose logs -f bot.
  • Standard rescue commands: /admin_cancelmatch, /admin_rewind, /admin_clearqueue, /admin_resetcooldown.
  • /admin_export dumps the full dataset as CSV for offline analysis.
  • /admin_seasonreset archives the current season (full players + matches snapshot into seasons table) and resets stats.

Status

Bot is finished and proven in production. Due to unrelated community disputes I am no longer involved with that server — see the blog post for the story. The bot itself is complete and ready to be redeployed for any Marvel Rivals community that would actually use it.

  • Finished and production-proven.
  • Currently homeless. If you run a Rivals community and want it, reach out.
  • Rough pricing: free / near-free if the deployment doubles as my beta playground (see About).

What I took away

  • SQLite handles "small multiplayer server" workloads effortlessly — JSON was the wrong default; the mid-build migration paid for itself immediately.
  • Interactive dashboards (buttons + modals + dropdowns) beat text commands for anything staff use daily.
  • Systematic audit rounds catch about 3x more bugs than the same time on feature-driven testing.
  • Failover is worth the effort even for a Discord bot — downtime during a live tournament is community-trust damage you do not recover from.
  • Never write the code before the legals are signed.
toad mascot