Web-based draft tool for Suomi OW Overwatch tournaments. Captains pick maps and ban heroes in real time through shared links, while admins control the match from a separate panel. A stream overlay is included for broadcasting.
Live at sowdraft.fi
- Real-time draft — map picks, hero bans, and side selections sync instantly across all participants via WebSockets
- Match phases — ready check → map pick → side selection (Hybrid/Escort) → hero bans → result submission → repeat
- Timed turns — 60-second timer per map/hero-ban turn, 30 seconds for side selection; auto-action fires on timeout
- Result confirmation — both team captains must agree on the map score; auto-approves after 10 minutes if unanswered
- Detailed scoring — point scores (e.g. 3-2) for Control, Escort, Flashpoint and Hybrid; win/loss for Push
- Admin panel — create matches, edit live matches, view active games and full match history
- Tournament support — group matches under named tournaments and filter stats/history by tournament
- Statistics — hero ban rates, map pick rates, side selection percentages, team win/loss records
- Discord integration — posts full match results (map-by-map breakdown with bans) to a Discord channel automatically
- Stream overlay — dedicated read-only view for OBS/streaming software
- Content management — add/remove maps, heroes (with images), and team logos through the admin panel
| Layer | Technology |
|---|---|
| Server | Node.js 20, Express 4 |
| Real-time | Socket.IO 4 |
| Database | SQLite 3 (WAL mode) |
| Auth | express-session, bcryptjs |
| Security | Helmet, express-rate-limit, express-validator |
| File uploads | Multer (memory storage) |
sowdraft/
├── server.js # Entry point — wires middleware, routes, sockets
├── lib/
│ ├── auth.js # requireAuth middleware + admin user
│ ├── constants.js # Timer durations, port, etc.
│ ├── db.js # SQLite connection + promise helpers
│ ├── discord.js # Discord result posting
│ ├── gameState.js # In-memory match/tournament/hero state + all game logic
│ ├── initDB.js # Schema creation, data migration, server startup
│ ├── upload.js # Multer image upload config
│ └── utils.js # escapeHTML, validation middleware
├── routes/
│ ├── auth.js # /login, /logout, /auth-status
│ ├── assets.js # /maps, /heroes
│ ├── teams.js # /api/teams
│ ├── mapPools.js # /map-pools
│ ├── matches.js # /create-match, /active-games, /match-history, admin endpoints
│ ├── tournaments.js # /tournaments
│ └── stats.js # /stats
├── sockets/
│ └── index.js # All Socket.IO event handlers
├── public/
│ ├── admin.html/.js # Admin panel (login-protected)
│ ├── user.html # Match list / home page
│ ├── teams.html # Captain draft view
│ ├── stream.html # Read-only stream overlay
│ ├── match.js # Shared client-side draft logic
│ ├── styles.css
│ └── images/ # Map thumbnails, hero portraits, team logos
└── data.db # SQLite database (gitignored)
- Node.js 20+ (see
.nvmrc) - npm
git clone <repo-url>
cd sowdraft
npm installCopy the example below to a .env file in the project root:
# Required
SESSION_SECRET=change-this-to-a-long-random-string
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-this-password
# Discord (optional)
DISCORD_BOT_TOKEN=
DISCORD_RESULTS_CHANNEL_ID=The server refuses to start if SESSION_SECRET, ADMIN_USERNAME, or ADMIN_PASSWORD are missing.
npm start
# or on Windows:
launch.batThe server starts on http://localhost:3000. On first launch it creates the SQLite database and seeds maps and heroes from any existing image files.
| Variable | Required | Description |
|---|---|---|
SESSION_SECRET |
✅ | Secret key for signing session cookies. Use a long random string in production. |
ADMIN_USERNAME |
✅ | Username for the admin panel. |
ADMIN_PASSWORD |
✅ | Password for the admin panel. |
DISCORD_BOT_TOKEN |
☐ | Bot token for posting match results to Discord. Results are silently skipped if not set. |
DISCORD_RESULTS_CHANNEL_ID |
☐ | Discord channel ID to post results to. Defaults to 1230883826159452342. |
ASSET_VERSION |
☐ | Cache-busting string appended to static asset URLs. Defaults to the server start timestamp. |
NODE_ENV |
☐ | Set to production to enable Secure flag on session cookies (requires HTTPS). |
| URL | Who | Description |
|---|---|---|
/ |
Anyone | Home — lists active matches |
/admin |
Admin | Admin panel (login required) |
/teams?matchId=X&role=team1 |
Captain | Team 1 draft view |
/teams?matchId=X&role=team2 |
Captain | Team 2 draft view |
/stream?matchId=X&role=stream |
Streamer | Read-only overlay (no sounds, no action buttons) |
Links for all three roles are generated automatically when a match is created.
Each match progresses through the following phases in order, repeating per map until a team reaches the required win count.
ready-check → map → [side-selection] → [hero-ban] → result → map → ...
└─ complete
Both team captains click Mark as ready. The draft starts when both are ready.
The team whose turn it is picks a map from the pool. Each gamemode can only be picked once per match (resets if all gamemodes have been used). The loser of the previous map always picks next; Team 1 picks first on the opening map.
Timer: 60 seconds. A random eligible map is picked automatically on timeout.
The team that did not pick the map chooses Attack or Defense. On the first map this defaults to the opposite team of whoever picked.
Timer: 30 seconds. A random side is chosen automatically on timeout.
One ban per team per map, applied simultaneously in a single round (first one team, then the other). Rules:
- A hero cannot be banned by the same team in multiple maps.
- The two bans in a round must be from different roles (Tank / DPS / Support).
- The loser of the previous map bans first; Team 2 bans first on the opening map.
Timer: 60 seconds per ban. A random eligible hero is chosen automatically on timeout.
One team captain submits the map result:
- Control / Escort / Flashpoint / Hybrid — enter the point score (e.g. 3-2); winner is determined from the score.
- Push — select the winning team directly.
The opposing captain must confirm. If no confirmation arrives within 10 minutes, the result is auto-approved.
The admin can also override and submit results directly from the active games panel.
Access at /admin. Login credentials come from ADMIN_USERNAME / ADMIN_PASSWORD in .env. Checking Remember Me at login keeps the session alive for 30 days.
| Tab | Description |
|---|---|
| Create Match | Set teams, format (FT1/2/3), map pool, optional hero bans, and tournament. Generates three links (Team 1, Team 2, Stream). |
| Active Games | Live table of ongoing matches. Edit match details, manually submit results, copy links, or end a match. |
| Tournaments | Create and rename tournaments. Matches can be tagged with a tournament when created. |
| History | All completed matches with map-by-map breakdowns and hero bans. Filterable by tournament. |
| Teams | Add/remove teams and upload team logos. |
| Maps & Heroes | Add/remove maps (with gamemode and optional thumbnail) and heroes (with role and optional portrait). Changes take effect immediately in new matches. |
| Statistics | Aggregated hero ban counts, map pick counts, Attack vs. Defense preference, and team win/loss records. Filterable by tournament. |
When a match completes, the server posts a formatted result to Discord:
Team A 2 - 1 Team B
🏆 Winner: Team A
**Map breakdown**
King's Row (Hybrid) Pick: Team A
**Bans**
- Team A: Tracer
- Team B: Ana
**Result: Team A (3-2)**
Nepal (Control) Pick: Team B
...
If the message exceeds Discord's 2000-character limit it is automatically split into multiple messages.
Setup:
- Create a Discord bot at https://discord.com/developers/applications
- Add the bot to your server with Send Messages permission in the target channel
- Set
DISCORD_BOT_TOKENandDISCORD_RESULTS_CHANNEL_IDin.env
Everything is stored in data.db (SQLite, gitignored). The database is created automatically on first launch.
| Table | Contents |
|---|---|
matches |
All matches (active and completed) as JSON blobs with indexed metadata |
tournaments |
Tournament names and IDs |
map_pools |
Saved map pool presets |
maps |
Available maps with gamemode and optional image URL |
heroes |
Available heroes with role and optional image URL |
teams |
Team names and logo URLs |
Uploaded images are stored in public/images/ under maps/, heroes/, or logos/ subdirectories. Filenames use UUIDs to prevent collisions.
Active match state is mirrored in memory for fast access and written to SQLite after every meaningful state change. On server restart, all state is reloaded from the database and active-game timers are resumed.
- Session cookies are
HttpOnly,SameSite=Strict, andSecurein production - All admin endpoints require an authenticated session (
requireAuth) - Login is rate-limited to 20 attempts per 15-minute window
- Input is validated with
express-validatoron all write endpoints DELETE /map-pools(clear all) requires{ confirm: true }in the request body to prevent accidental data loss- Uploaded image filenames are UUIDs — user-supplied filenames are never used on disk
- Content Security Policy is enforced via Helmet