9.5 KiB
CLAUDE.md
Running the app
npm install
npm run dev # development (nodemon)
npm start # production
Requires .env — copy from .env.example. Database is created automatically on startup (better-sqlite3 runs migrations on init).
Stack
- Node.js · Express · EJS templates
- better-sqlite3 — SQLite, synchronous, simple
- ws — WebSocket server for pushing alerts to overlay clients
- tmi.js — Twitch IRC, runs in the browser on the overlay page (no server-side IRC)
- multer — file uploads
- uuid — UUID generation
- dotenv — env vars
- express-session — session management (signed cookie)
Architecture
Twitch ──EventSub WebSocket──► src/eventsub.js ──► dispatch.js ──► ws.js ──► OBS overlay
Twitch ──IRC (tmi.js in browser) ────────────────────────────────────────► OBS overlay
eventsub.jsopens a WebSocket to Twitch on startup, managed as a module-level singleton- On notification it calls
dispatch.dispatchEvent() dispatch.dispatchEvent()queries matchingevent_actionsrows, checks cooldowns, logs toactivity_log, callswss.broadcast()wss(ws.js) holds all active WebSocket connections from/ws/overlay- The overlay JS receives the broadcast and plays the action
- Chat messages come directly from tmi.js running in the browser — no server relay
Project structure
src/
app.js — Express app setup, middleware, router mounting
server.js — HTTP server + WebSocket server startup, EventSub init
db.js — better-sqlite3 setup, migrations, singleton helpers
eventsub.js — Twitch EventSub WebSocket client (module singleton)
dispatch.js — event → action resolution, cooldowns, DB log, broadcast
ws.js — WebSocket connection manager
cooldowns.js — in-memory cooldown tracking
routers/
auth.js — /auth/* (Twitch OAuth)
dashboard.js — / (activity log, EventSub status)
overlay.js — /overlay/:uuid, /overlay/:uuid/preview, /ws/overlay
actions.js — /actions CRUD
media.js — /media upload/delete
layout.js — /layout editor + /layout/positions
settings.js — /settings
views/
layout.ejs — base shell (nav, Pico CSS)
login.ejs
dashboard.ejs
overlay.ejs — OBS browser source
overlay_preview.ejs
layout_editor.ejs
actions.ejs
media.ejs
settings.ejs
public/
media/ — uploaded files (gitignored)
Key conventions
No passwords anywhere. Auth is Twitch OAuth only. Session stored via express-session with a signed cookie. req.session.userId stores the DB user ID after login.
Singleton tables. settings and overlay_layout each have one row (id=1). db.getSettings() and db.getLayout() create the row on first call if missing.
Cooldowns are in-memory. cooldowns.js tracks { actionId: triggeredAt }. Restarting the server resets all cooldowns. Intentional — DB cooldowns not worth the complexity.
Multiple actions per event. dispatchEvent() loops through ALL matching event_actions rows and fires each one (subject to its own cooldown). No "first match wins".
Event matching rules:
event_type="command":event_detailmust match the command name (case-insensitive)event_type="bits": ifevent_detailset, treat as minimum bits thresholdevent_type="channel_points":event_detailis the reward ID; null matches any rewardevent_type="sub"/"gift_sub":event_detailis tier ("1000", "2000", "3000"); null matches anyevent_type="follow"/"raid":event_detailnot used
UUID overlays. settings.overlay_id is a UUID generated on first run. OBS browser source URL is /overlay/:uuid. Future multi-user support creates multiple settings rows.
Route map
| Method | Path | File | Notes |
|---|---|---|---|
| GET | / |
routers/dashboard.js | Activity log, EventSub status |
| GET | /auth/login |
routers/auth.js | Login page |
| GET | /auth/twitch |
routers/auth.js | Redirect to Twitch OAuth |
| GET | /auth/callback |
routers/auth.js | Exchange code, save user, start EventSub |
| GET | /auth/logout |
routers/auth.js | Clear session |
| GET | /overlay/:uuid |
routers/overlay.js | Live OBS browser source |
| GET | /overlay/:uuid/preview |
routers/overlay.js | Mock preview for layout editor |
| WS | /ws/overlay |
routers/overlay.js | Bidirectional: commands up, actions down |
| GET/POST | /actions |
routers/actions.js | List and create actions |
| POST | /actions/:id/test |
routers/actions.js | Fire immediately, skip cooldown |
| POST | /actions/:id/toggle |
routers/actions.js | Enable/disable |
| DELETE | /actions/:id |
routers/actions.js | Delete |
| GET/POST | /media |
routers/media.js | Upload, list |
| DELETE | /media/:id |
routers/media.js | Delete file + DB row |
| GET/POST | /layout |
routers/layout.js | Style form |
| POST | /layout/positions |
routers/layout.js | JSON, called by drag JS |
| GET/POST | /settings |
routers/settings.js | Channel, gap audio, custom CSS/JS |
| POST | /settings/reconnect |
routers/settings.js | Restart EventSub |
Overlay CSS variable system
All overlay positioning and styling uses CSS custom properties rendered into overlay.ejs and overlay_preview.ejs from the overlay_layout row:
| Variable | Controls |
|---|---|
--chat-left, --chat-top |
Chat box position (%) |
--chat-width, --chat-height |
Chat box size (%) |
--chat-font-size |
Chat font size (px) |
--chat-font-family |
Chat font family |
--chat-bg-opacity |
Chat message background opacity |
--chat-text-color |
Chat message text colour |
--chat-border-radius |
Message border radius (px) |
--alert-left, --alert-top |
Alert position (%, centered via transform) |
--alert-font-size |
Alert font size (px) |
--alert-bg-opacity |
Alert background opacity |
--alert-text-color |
Alert text colour |
The layout editor (/layout) overlays drag handles on an iframe pointing to /overlay/:uuid/preview. Same-origin, so the editor sets CSS variables directly on the iframe's document for live preview. Saved via POST /layout/positions.
Gap / new-message audio
Configured in settings. When a chat message arrives in the overlay, tmi.js checks:
- Is
gap_audio_enabledtrue andgap_audio_urlset? - Has at least
gap_audio_min_silenceseconds passed since the last message? - If yes: play the audio. Volume = linear interpolation between
gap_audio_min_volumeandgap_audio_max_volumebased on silence duration vsgap_audio_loud_after.
All logic runs in the browser. The server only serves the config.
Custom events dispatched to overlay JS
When the overlay WebSocket receives an action broadcast:
document.dispatchEvent(new CustomEvent(`bashyoverlay:${msg.event_type}`, { detail: msg }));
Available: sub, gift_sub, bits, channel_points, command, follow, raid.
msg shape: { event_type, action_type, media_url, alert_text, username }.
Database schema
users — Twitch OAuth user (twitch_id unique)
settings — singleton id=1: overlay_id, channel, gap audio, message_timeout, custom_css/js
overlay_layout — singleton id=1: widget positions and styles
media_files — uploaded sounds/videos
event_actions — event → action mapping (media_file_id FK)
activity_log — append-only event log
EventSub subscriptions
| Twitch event | Version | Condition | Scope |
|---|---|---|---|
channel.subscribe |
1 | broadcaster_user_id |
channel:read:subscriptions |
channel.subscription.message |
1 | broadcaster_user_id |
channel:read:subscriptions |
channel.subscription.gift |
1 | broadcaster_user_id |
channel:read:subscriptions |
channel.cheer |
1 | broadcaster_user_id |
bits:read |
channel.channel_points_custom_reward_redemption.add |
1 | broadcaster_user_id |
channel:read:redemptions |
channel.follow |
2 | broadcaster_user_id + moderator_user_id |
moderator:read:followers |
channel.raid |
1 | to_broadcaster_user_id |
none |
Token refresh triggered on 401 during subscription.
Production deployment
Runs behind Apache on the VPS as a reverse proxy to Node on port 3000.
Production domain: https://overlay.bashynx.com
Minimal Apache vhost config (requires mod_proxy, mod_proxy_http, mod_proxy_wstunnel):
<VirtualHost *:443>
ServerName overlay.bashynx.com
ProxyPreserveHost On
ProxyPass /ws/overlay ws://127.0.0.1:3000/ws/overlay
ProxyPassReverse /ws/overlay ws://127.0.0.1:3000/ws/overlay
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
# SSL config here (certbot/Let's Encrypt)
</VirtualHost>
The /ws/overlay must be proxied separately with ws:// via mod_proxy_wstunnel — if it's not listed before the catch-all /, WebSocket connections will fail. Enable the required modules with:
sudo a2enmod proxy proxy_http proxy_wstunnel
sudo systemctl reload apache2
Twitch requires HTTPS for OAuth redirect URIs in production. Register both in the Twitch dev console:
http://localhost:3000/auth/callback(dev)https://overlay.bashynx.com/auth/callback(production)
Not yet implemented
- Alert variable interpolation (
{username}in alert text) - Mod access (streamer-only login for now)
- Chat commands triggering actions
- Stripe / PayPal / ETH payment notifications
- Multiple overlays (multi-user; UUID foundation is in place)
- HudFX integration