heystreamer/CLAUDE.md
Bashy 6c7facadce
Some checks failed
CI / check (push) Has been cancelled
rework
2026-04-26 23:00:55 +03:00

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.js opens a WebSocket to Twitch on startup, managed as a module-level singleton
  • On notification it calls dispatch.dispatchEvent()
  • dispatch.dispatchEvent() queries matching event_actions rows, checks cooldowns, logs to activity_log, calls wss.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_detail must match the command name (case-insensitive)
  • event_type="bits": if event_detail set, treat as minimum bits threshold
  • event_type="channel_points": event_detail is the reward ID; null matches any reward
  • event_type="sub"/"gift_sub": event_detail is tier ("1000", "2000", "3000"); null matches any
  • event_type="follow"/"raid": event_detail not 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:

  1. Is gap_audio_enabled true and gap_audio_url set?
  2. Has at least gap_audio_min_silence seconds passed since the last message?
  3. If yes: play the audio. Volume = linear interpolation between gap_audio_min_volume and gap_audio_max_volume based on silence duration vs gap_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