# CLAUDE.md ## Running the app ```bash 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: ```javascript 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 ```sql 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`): ```apache 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) ``` 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: ```bash 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