Twitch Stream Toolkit

A config-driven Twitch bot with a real-time OBS overlay system and a Twitch-authenticated web admin panel, built on Node with zero web dependencies and deployed on AWS behind automatic HTTPS. Paired with a companion Chrome extension that automates on-stream privacy protection, and a separate automated pipeline that downloads, processes, and uploads every finished VOD to YouTube. Built for a live channel (internally “bob-a-bot”), then generalized into a reusable, secret-free template.

Try the Admin Panel (Sample) → See the VOD Uploader → See the Privacy Guard →
Status Live in Production

The bot and overlays run 24/7; the VOD pipeline runs on a schedule after each stream. Overlays are public, the admin panel sits behind Twitch login.

Build Type Solo

Design, build, deploy, and ops, end to end, across the bot, the extension, and the upload pipeline.

Primary Focus

Full-stack + DevOps: a Node bot, real-time overlays, an OAuth admin panel, a self-hosted cloud deployment, and an automated VOD-to-YouTube pipeline.

Channels It Runs For

Live: Runs 24/7 as “bob-a-bot” on twitch.tv/bobavillain, the channel it was originally built for.

Scalable: Because the whole platform is config-driven, the same bot can be stood up for additional channels from a single JSON file, with no code changes.

Project Overview

Purpose: One self-hosted system that covers counters, song requests, alerts, moderation, on-stream overlays, on-stream privacy, and the after-stream VOD upload, fully controllable by the streamer and their mods.

Problem: Streamers usually stitch together several paid third-party services to cover these features, plus a manual video-editing/upload routine after every stream. This replaces all of it with a single system the streamer owns and controls.

Approach: Built for one specific channel, then generalized into a config-driven template so the same platform can be stood up for anyone. The bot, the extension, and the VOD pipeline are three separate codebases that share one Twitch identity, not one monolith.

Core Features

Feature setEverything the bot and its admin panel cover, grouped by area.
CategoryIncludes
Chat & Channel Points
  • Counters via Channel Points, bit cheers, and chat commands
  • Custom commands matched on the first word
  • Timers and timed challenges
  • Clips and stream title / category controls
Moderation & Community
  • Shoutouts (/announce plus native Twitch popup)
  • Follower thanks and follow-age lookups
  • Chat moderation: timeout, ban, and unban
  • Redeem logs and a game backlog
OBS Overlays
  • Three independent, movable browser sources
  • Countdown timer that cycles through multiple timers
  • Live counters ticker
  • Spotify now-playing card that auto shows / hides
Web Admin Panel
  • Twitch login, gated to broadcaster, allowlist, and mods
  • Live counters and command / quote / game management
  • Timeout, ban, unban, and redeem history
  • Diagnostics page that fires overlay tests
Try a sample of the admin panel →
External Services
  • Spotify song requests
  • DeepL live translation
  • Weather (Open-Meteo) and other no-key APIs
  • Each integration stays off until it's configured
Live Events (EventSub)
  • Channel Point redemptions
  • Raids and go-live notifications
  • New follows and category changes
  • Delivered over an EventSub WebSocket listener

Overlay Preview

Countdown timer overlay showing a label, a quoted note, a 0:09 countdown, and a progress bar
Countdown timer: label, note, live countdown, and a progress bar, cycling through multiple configured timers.
Live counters ticker overlay showing two counters, Spicy at 1 and Pushups at 11
Live counters ticker: one card per active counter, driven by Channel Points, bit cheers, and chat commands.
Spotify now-playing overlay showing the current track title and artist with a progress bar
Spotify now-playing card: current track and artist with a progress bar, auto showing and hiding with playback.

How It Works

A single long-lived Node process connects to Twitch chat and EventSub and starts an embedded web server in the same process, with no separate services to orchestrate.

Bot architectureOne process, three jobs, sitting behind a reverse proxy on the same box that also runs the VOD pipeline's upstream data source.
LayerRoleWhat it does
ProcessBot CoreOne long-lived Node process, connected to Twitch chat and EventSub.
Web LayerEmbedded ServerServes the OBS overlays over SSE, serves the Twitch-OAuth admin panel, and exposes a small JSON API the panel calls.
EdgeReverse ProxyCaddy sits in front on port 443 for HTTPS and proxies to the bot on an internal port, keeping the Node process off the public interface.
StateFlat-File PersistenceCounters, commands, quotes, and timers persist to JSON files on disk, one file per feature. No database to run.

VOD Uploader

Purpose: A separate, standalone pipeline that turns every finished stream into a published YouTube video with no manual editing or upload step. It runs on a schedule after every stream, downloads the VOD, trims dead air, prepends a rendered title card, builds chapters, and uploads it, landing as Unlisted for a quick look before going public.

Integration: The bot's own vodChapters feature is the only link between the two systems: it subscribes to Twitch's channel.update EventSub and calls Create Stream Marker whenever the category changes, plus once at stream start. The VOD Uploader reads those markers back after the stream via Get Stream Markers and converts them into YouTube chapter timestamps. The bot has no other awareness of the uploader and keeps working fine if that project is ever removed.

Upload pipelineOne scheduled run, start to finish, triggered by Windows Task Scheduler every 6 hours.
StageWhat happens
DetectPolls Twitch for the channel going offline plus a finished, unprocessed VOD; each scheduled run does one check and exits.
DownloadPulls the full VOD with yt-dlp, TTY-aware so progress is compact live and newline-verbose in the log file.
Processffmpeg trims leading/trailing dead air (no mid-stream cuts) and prepends a rendered title card: word-wrapped title, date, and Twitch link over a custom backdrop.
ChaptersReads the bot's Twitch stream markers back and converts them into YouTube's chapter-timestamp format, forcing the first chapter to 0:00.
UploadResumable upload via the YouTube Data API, landing as Unlisted; processed.json tracks the VOD ID so a re-run never double-uploads.

VOD Uploader Decisions

Device-Code Flow Over Delegated Access. Twitch and YouTube both authenticate over the OAuth device-code flow: a short code and link go to the streamer, who approves directly from her own device as the account owner. Chosen after a Brand-Account-Manager approach, authenticating as a delegate rather than the owner, turned out to be unreliable in practice.

Unlisted, Not Public. Uploads land as Unlisted rather than fully public, so every VOD gets a look before it's shared, without turning the pipeline into a manual publish step. Started as Private, moved to Unlisted once the review step proved reliable in practice.

A Duration Cap Before the Expensive Steps. YouTube hard-caps a single upload at 12 hours even on a verified account. Raw duration is checked against an 11.5-hour cap right after download, before the costly silence-detect, encode, and upload steps run, and a too-long VOD is recorded in processed.json's skipped map instead of retried forever.

Streamer Privacy Guard

An unpacked Chrome extension that redacts personal information and blocks mail sites, but only while the stream is live. Offline, it is fully unloaded rather than merely disabled: no content script runs, no blocking rules exist, nothing executes on any page. Not published to the Chrome Web Store by design; loaded unpacked on the streaming machine.

Google search results for a coffee shop with the map thumbnail blurred and the address and hours lines blacked out while the stream is live
Search results redacted live: the map thumbnail is blurred and location detail lines are blacked out, matching the Redaction and Location Guard behavior.
Browser tab showing a Hidden while live placeholder in place of Gmail, explaining the site is blocked while streaming and will restore automatically once the stream ends
A mail host swapped for the self-restoring placeholder: nothing is closed, and the tab returns to Gmail on its own once the stream ends.
How it behavesSix pieces working together, all scoped to the live signal.
AspectBehavior
SignalPolls a public /live endpoint added to the existing bot server, backed by an in-memory value that Twitch EventSub keeps current, so checking it costs nothing. A missing or stale signal is treated as live, so a dropped connection can never silently un-blur the screen.
RedactionA personal list of exact strings (name, address, old handles) plus generic safety-net patterns for emails, phones, addresses, and Luhn-checked card numbers. Three looks are available: blend, black bars, and a tunable blur, chosen per field.
LocationBlurs and desaturates map surfaces across Google Maps, embedded iframes, and Leaflet/Mapbox/OpenLayers widgets, and hides the location furniture search engines and shops wrap around a city, down to context-matched ZIPs and a curated place-name list.
BlockingMail hosts redirect to a local placeholder that carries the original URL and navigates back on its own once the stream ends. Nothing is closed and no draft is lost; the tab title is blanked before the redirect so the address never flashes.
ModesAuto follows the live signal for normal use. Always On arms it for testing or a minute before going live. Always Off stays fully dormant. Both forced modes badge purple so a test can never be mistaken for a real live signal.
Escape HatchA configurable hold-to-peek shortcut reveals redactions only while held, with a warning banner across the top. Releasing the key, switching tabs, or leaving the window all re-hide immediately.

Privacy Guard Decisions

Unknown State Means Stay Redacted. A missing check or one older than two minutes is treated the same as live. Worst case is mild annoyance browsing off-stream; the alternative worst case is a doxx on stream, so the fallback only ever leans toward more redaction, never less.

Text Is Barred or Blended, Never Blurred. A blurred string can be undone by rendering candidate text at the same radius and matching against the frame, since the alphabet is small and known, a documented attack a VOD gives unlimited time to run. Maps get heavy blur instead, since deblurring imagery isn't practical the same way.

Dormant Means Unloaded, Not Idle. The content script is registered when live and unregistered when offline through chrome.scripting, so when the stream is down no extension code runs on any page at all rather than sitting idle on every tab.

Two Independent Blocking Paths. Chrome silently refuses declarativeNetRequest redirects for null-initiator navigations, like a typed URL, with no error anywhere. A second, independent tabs listener does the same job so either path alone can fail without the block failing.

Tools + Build

Node.js 18+ES ModulestwurpleTwitch Helix APITwitch EventSubOAuth 2.0OAuth Device Code FlowServer-Sent EventsNode httpcrypto HMAC SessionsSpotify Web APIDeepL APIVanilla HTML/CSS/JSAWS EC2CaddyLet's Encrypt TLSpm2cron BackupsUptimeRobotFlat-file JSONChrome Extension (MV3)declarativeNetRequestchrome.scriptingMutationObserveryt-dlpffmpegYouTube Data API v3Windows Task Scheduler

Decisions Worth Calling Out

Config-Driven, Not Hard-Coded. Around 40 features toggle and tune through a single JSON file, so a non-developer can reconfigure the bot and it generalizes cleanly into a template. This is the core architectural bet.

Zero Web Dependencies. Routing, SSE, sessions, OAuth, and CSRF are all built on Node built-ins plus fetch: no Express, no framework, no build step. Tiny footprint and minimal supply-chain surface.

SSE-Driven Overlays. Overlay updates are one-directional, so Server-Sent Events beat WebSockets here: simpler, auto-reconnecting, no library. Countdowns tick locally in the browser from a server-sent end timestamp, so they stay smooth and network-light.

Security by Least Exposure. Overlays are gated by a secret key; the admin panel is Twitch-OAuth gated to an allowlist and mods, with HMAC-signed SameSite=Lax session cookies and a custom-header CSRF check. No secrets ever reach the client.

Deployment & Reliability

Where each piece actually runsThe bot and the VOD pipeline are deliberately on different machines.
AspectDetail
CloudThe bot runs on an AWS EC2 instance (Amazon Linux) with pm2 for process management and reboot persistence, on a static Elastic IP with DNS via a subdomain.
HTTPSA Caddy reverse proxy provisions and renews Let's Encrypt certificates automatically, so HTTPS is hands-off.
Backups & MonitoringNightly cron backups protect the bot's flat-file data, and UptimeRobot health-checks a /health endpoint so outages surface immediately.
VOD PipelineRuns locally on Kevin's PC, not on the EC2 box: ffmpeg encoding and multi-gigabyte VOD transfers don't belong on the small always-on instance that runs the bot. Triggered by Windows Task Scheduler every 6 hours instead of running as a long-lived service.
ReuseExtracted a generic, secret-free bot template from the live instance without taking it down, genericizing hard-coded labels into config along the way.

Engineering Challenges

Multi-Timer Reconciliation

Several timers can run at once (challenge redeems plus reminders). The overlay reconciles a live list over SSE and cycles through them (~15s each), giving each its own style and an independent local countdown.

Readable Over Any Scene

Overlays have to read over arbitrary gameplay footage. Solved with solid tile backings and text outlines (no drop shadows), tested against both bright and dark scenes.

A Corrupted Source Mid-Encode

Symptom: A background scheduled VOD-Uploader run and a manually-triggered one ended up processing the same VOD at the same time. The manual run's download overwrote the source file the scheduled run's ffmpeg was actively reading, mid-encode: the file size ballooned from a clean 8.58GB to 16.39GB with no crash and no error, just silently corrupted output.

Fix: caught by comparing file sizes, not an error message. Stopped both processes, wiped the working directory, confirmed nothing false had been recorded, and reran clean. No automated lock exists yet: the rule going forward is discipline, not enforcement. Never trigger a manual run while the scheduled task might already be active.

More projects like this

← Back to all projects