Unified Stream Chat is a production-ready multi-platform chat layer for streamers. Add a streamer by name and the channels they use, and their Twitch, X, and Kick chats merge into one live feed — color-tagged per host — with a clean transparent OBS overlay. No login or OAuth from the streamers themselves.
Built for multi-host shows: several streamers' chats in one box, each in their own color, so a co-streamed broadcast reads as a single conversation.
Live demo: https://unified-stream-chat.vercel.app
Clean OBS chat box — multiple hosts merged into one feed, each color-tagged:
Dashboard: add chat watchers, see the unified source-labeled feed:
Transparent OBS lower-third and vertical rail overlays are also included:
- Chat watchers: add a streamer by name + channels and aggregate their public chats — no login or OAuth from the streamer, with per-platform on/off toggles.
- Twitch chat through the official IRC WebSocket (multi-channel).
- X live-broadcast chat relayed by a local browser bridge (X exposes no API for it) — the in-stream chat, not mentions.
- X live-broadcast chat via an optional local browser bridge (
npm run x-live) — X has no API for it. - Kick chat through official
chat.message.sentwebhook events, auto-subscribed per watched channel. - One normalized live feed with source labels and per-host colored identity tags.
- Transparent
/overlaypage for OBS browser sources. - Multiple overlay shapes: full-width lower third, vertical side rail, and compact corner box.
- Server-sent events for low-latency dashboard/overlay updates, with polling fallback.
- Optional write auth for production.
- Optional Upstash Redis REST persistence for multi-instance hosting.
- Optional Kick RSA signature verification.
- Docker-ready and dependency-light.
git clone https://github.com/psychedelanon/unified-stream-chat.git
cd unified-stream-chat
npm install
npm run setup
npm run devOpen:
- Dashboard:
http://127.0.0.1:8787/ - OBS overlay:
http://127.0.0.1:8787/overlay - OBS right rail:
http://127.0.0.1:8787/overlay?layout=rail&position=right&messages=5 - Kick webhook:
http://127.0.0.1:8787/api/kick/webhook
Click Seed All or Demo Pulse to see all three source labels immediately.
Run a setup check at any time:
npm run doctorThe core flow. Add a streamer under Chat Watchers — name, color, and any of their channels (X handle, Twitch channel, Kick channel) — and their public chats merge into the feed tagged with their color. The streamer never logs in or connects anything:
- Kick: the server subscribes to
chat.message.sentwebhooks for the channel using app-token credentials. Fully automatic from then on. - Twitch: the dashboard joins the channel over Twitch's official IRC WebSocket (all watched channels share one socket).
- X: the in-stream live-broadcast chat, relayed by the bridge (see below). X exposes no API for it, so a small helper runs on the streaming PC and attaches to each broadcast automatically. The X handle on a watcher is the bridge target and drives the host's color tag.
Each watcher row shows X / Twitch / Kick toggle chips; muting a platform is enforced server-side. Multiple watchers make this a multi-host show tool: every message carries both a platform label and a colored host tag.
- Add a Browser Source.
- URL — the chat box is the recommended overlay (copy the exact URL from
the dashboard's OBS Overlays panel, or use):
- Chat box:
http://127.0.0.1:8787/overlay?layout=box - Lower third:
http://127.0.0.1:8787/overlay - Right rail:
http://127.0.0.1:8787/overlay?layout=rail&position=right&messages=5 - Compact corner:
http://127.0.0.1:8787/overlay?layout=compact&position=bottom-right&messages=3
- Chat box:
- Size the source to your scene (the chat box fills whatever width/height you give it; the lower third / rail / compact layouts assume a 1920x1080 source).
- Enable transparent background if OBS prompts for it.
The chat box is a clean scrolling panel (newest at the bottom, like real chat),
showing the latest ~14 messages with platform badges and per-host color tags.
Add &title=YourShow to brand the header. The lower third, rail, and compact
corner layouts are alternatives for shows that already have a chat element.
Copy .env.example to .env and configure what you need:
PUBLIC_BASE_URL=https://your-domain.example
STREAM_CHAT_ADMIN_TOKEN=generate-a-long-random-token
X_BEARER_TOKEN=optional-x-api-bearer-token
UPSTASH_REDIS_REST_URL=optional-upstash-url
UPSTASH_REDIS_REST_TOKEN=optional-upstash-token
KICK_PUBLIC_KEY=optional-kick-public-key
KICK_CLIENT_ID=optional-enables-kick-watcher-auto-subscribe
KICK_CLIENT_SECRET=optional-enables-kick-watcher-auto-subscribe
Run with Node:
npm install --omit=dev
npm run setup
npm startRun with Docker:
docker compose up -d --buildFor Kick webhooks on a local machine, expose the app with a tunnel:
cloudflared tunnel --url http://127.0.0.1:8787Then set the Kick app webhook URL to:
https://your-tunnel.trycloudflare.com/api/kick/webhook
X exposes no API for the chat inside a live broadcast, so the bridge reads it from a real signed-in browser (X live chat rides Periscope chat infra; the parser is calibrated to that frame format). The streamers never log in — the bridge only reads the public chat every viewer sees, and its browser just needs some X session (a burner works).
Setup on one PC:
npm run x-loginonce — sign into any X account, close the window. The session persists in.local/x-chat-profile.- Configure
.local/x-live.config(see.env.examplefor theX_LIVE_*keys), or copy the ready-made command from the dashboard's X Live Chat panel. npm run x-live— it watches the configured account, attaches to each broadcast as it goes live, and relays chat tagged with the host's color.
Optionally drop a shortcut to scripts\x-live-forever.bat into
shell:startup so it survives reboots and crashes. This is a power feature —
the normal watcher flow never needs it.
GET /health
GET /api/config
GET /api/messages
GET /api/events
GET /api/connections?room=...
POST /api/hosts
POST /api/hosts/toggle
DELETE /api/connections?room=&profile=&platform=
POST /api/ingest
POST /api/messages
DELETE /api/messages
GET /api/x/recent?query=...
POST /api/kick/webhook
When STREAM_CHAT_ADMIN_TOKEN is set, write routes require either:
Authorization: Bearer <token>
x-stream-chat-token: <token>
Kick webhook writes are accepted without the admin token so Kick can call them directly. Set KICK_PUBLIC_KEY in production if you want signature verification.
npm run check
npm run seed
npm run verifynpm run verify checks:
- API state has Twitch, X, and Kick messages.
- Dashboard renders on desktop.
- Dashboard renders on mobile without horizontal overflow.
- OBS overlay renders at 1920 x 1080.
- No browser console errors or visible loading/error/empty states when data exists.
Screenshots are written to .local/verification/.
For deployed apps, run:
$env:STREAM_CHAT_BASE_URL = "https://your-live-app.example"
npm run smoke:liveSeed a deployed demo directly:
node scripts\seed-demo.mjs https://your-live-app.exampleSee docs/DEPLOY.md. The repo includes:
Dockerfiledocker-compose.ymlrender.yamlProcfilevercel.jsonandapi/stream.jsfor demo/preview deployments- Local verification scripts:
npm run check,npm run doctor,npm run verify, andnpm run smoke:live
The Sproto stream can consume this standalone app by setting:
$env:SPROTO_CHAT_OVERLAY_URL = "http://127.0.0.1:8787/overlay"
cd ../bitcoinaqua-sproto-stream
node scripts\obs-layout-sproto-gameplay.mjsSee docs/BITCOINAQUA.md.



