Modern, transparent Chatango chat overlay for OBS, vMix, Streamlabs and any streaming software that supports Browser Sources.
A self-hosted alternative to jChat / StreamElements chat boxes — but for Chatango rooms instead of Twitch / Kick / YouTube. Bot connects anonymously, no account required. Real-time over WebSocket. Fully customisable. Multi-room. Looks great on top of any scene.
yourdomain.tld/overlay ← config panel (manage rooms, build OBS URL)
yourdomain.tld/overlay/view ← the transparent overlay (paste this into OBS)
- 100% transparent background — no Custom CSS in OBS, just paste & go
- Bubble or plain mode — with or without a translucent background per message
- Adjustable background opacity (0–100%)
- Avatars in a clean circle, configurable size, anons get no avatar
- Smooth message-in animation with fade-out after N seconds (configurable)
- 3 size presets: Small / Medium / Large — sets sensible font + avatar
- 12 fonts to choose from: Inter, Roboto, Lato, Noto Sans, Open Sans, Montserrat, Bebas Neue, Press Start 2P, Comfortaa, Source Code Pro, Wallpoet, Indie Flower (all from Google Fonts, preloaded)
- Text stroke (outline) in 5 levels — great for bright scenes
- Drop shadow in 4 levels — for extra contrast on busy backgrounds
- Small caps and ALL CAPS modifiers
- Custom font and avatar size sliders — override the preset
- URLs ending in
.jpg,.jpeg,.png,.gif,.webp,.bmpor.avifare rendered as<img>tags directly in the message - Configurable max image height (default 180 px)
- Toggleable per-overlay (
?images=0to disable)
- Hide one or more bot accounts by name (
?hidebot=raszeibot,fbicat) - Hide all messages starting with
!(commands) - Hide all anonymous users
- Hide every avatar globally
- Username colour from chatango is respected (with bug workaround for the
upstream
chatango-libzfill bug — "FFF" used to render as #000FFF) - Message text colour from chatango is respected
- Both can be turned off individually if you'd rather have a uniform look
- Multi-room — one bot process serves any number of rooms; rooms can be added & removed live without restarting the bot
- Auto-reconnect WebSocket with exponential backoff (1 s → 15 s)
- Per-room ring buffer of the last 40 messages so a fresh viewer immediately sees activity
- Persistent room list in
data/rooms.jsonsurvives restarts - systemd unit included, hardened with
NoNewPrivileges/PrivateTmp - OBS-friendly: no Custom CSS needed, transparent by default
┌──────────────────┐
chatango.com ───websocket───────────► │ OverlayBot │
(s{N}.chatango.com:8081) │ (chatango.Client)│
│ joins all rooms │
└────────┬──────────┘
│ on_message
▼
┌──────────────────┐
│ ChatangoManager │
│ ‒ history buffer│
│ ‒ WS client pool│
└────────┬─────────┘
│ broadcast()
┌───────────────────────────┼───────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ OBS overlay #1 │ │ OBS overlay #2 │ │ another viewer│
│ (websocket) │ │ (websocket) │ │ (websocket) │
└────────────────┘ └────────────────┘ └────────────────┘
Single Python process. One bot is a singleton — this is why the
service runs with --workers 1.
Stack: Python 3.10+, FastAPI, uvicorn, neokuze/chatango-lib. Frontend: vanilla JS + CSS, zero frameworks, zero build step.
- Linux server (tested on Debian / Ubuntu)
- Python 3.10+
- nginx (for SSL termination & reverse proxy)
- A domain pointing at the server
# 1. Get the code
git clone https://github.com/YOUR_USERNAME/streamchango.git /opt/streamchango
cd /opt/streamchango
chmod +x run.sh
# 2. Python virtualenv
apt install -y python3-venv python3-pip git
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt
deactivate
# 3. Quick test (Ctrl+C when done)
./run.sh &
sleep 4
curl http://127.0.0.1:8765/overlay/api/health
# {"ok":true,"active_rooms":["chatraszei"]}
kill %1; wait
# 4. systemd
cp streamchango.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now streamchango
systemctl status streamchango
journalctl -u streamchango -f # live logsOpen your domain's nginx server block (e.g. /etc/nginx/sites-available/yourdomain)
and add the two ^~ /overlay location blocks from
nginx-streamchango.conf inside it.
The order matters: /overlay/ws/ MUST come before /overlay, and ^~
guarantees they take precedence over any / block you may have for another
app.
nginx -t && systemctl reload nginx- In your scene: + → Browser
- URL: copy from the Copy button on
https://yourdomain.tld/overlay - Width:
420, Height:720(or whatever fits your scene) - Custom CSS: leave empty — the page is already transparent
- Tick Refresh browser when scene becomes active
Build these in the panel UI, or hand-edit if you know what you want:
| Parameter | Values | Default | Description |
|---|---|---|---|
room |
<n> |
(required) | Chatango room name |
size |
small, medium, large |
medium |
Preset for font + avatar size |
font |
0–11 |
0 |
0=Inter, 1=Roboto, 2=Lato, 3=Noto Sans, 4=Open Sans, 5=Montserrat, 6=Bebas Neue, 7=Press Start 2P, 8=Comfortaa, 9=Source Code Pro, 10=Wallpoet, 11=Indie Flower |
fontsize |
0–48 |
0 (use preset) |
Override font size in px |
avatarsize |
0–80 |
0 (use preset) |
Override avatar size in px |
bg |
bubble, plain |
bubble |
Show translucent message background |
bgalpha |
0–100 |
55 |
Bubble background opacity (%) |
stroke |
0–4 |
0 |
Text outline: off / thin / medium / thick / thicker |
shadow |
0–3 |
1 |
Text shadow: off / small / medium / large |
animate |
0, 1 |
1 |
Smooth animation when new messages arrive |
fade |
0–300 |
30 |
Auto-hide messages after N seconds (0 = never) |
max |
5–100 |
30 |
Max visible messages on screen |
hidebot |
csv |
(none) | Comma-separated bot usernames to hide |
hidecmd |
0, 1 |
0 |
Hide messages starting with ! |
hideanon |
0, 1 |
0 |
Hide all anonymous users |
hideavatar |
0, 1 |
0 |
Hide all avatars |
images |
0, 1 |
1 |
Render image URLs as inline images |
imagesize |
80–400 |
180 |
Max image height in px |
smallcaps |
0, 1 |
0 |
Render text in small caps |
allcaps |
0, 1 |
0 |
Render text in ALL CAPS |
namecolor |
0, 1 |
1 |
Respect username colour from chatango |
msgcolor |
0, 1 |
1 |
Respect message text colour from chatango |
debug |
0, 1 |
0 |
Show connection status pill (top-right) |
Example URL with everything customised:
https://yourdomain.tld/overlay/view
?room=chatraszei
&size=large
&font=5
&bg=bubble
&bgalpha=60
&stroke=2
&shadow=2
&fade=30
&max=25
&hidebot=raszeibot
&hidecmd=1
&images=1
&imagesize=200
| Method | Endpoint | Description |
|---|---|---|
GET |
/overlay/api/health |
Health check + currently joined rooms |
GET |
/overlay/api/rooms |
List rooms (persisted + active) |
POST |
/overlay/api/rooms |
Add a room. Body: {"room":"<n>"} |
DELETE |
/overlay/api/rooms/{room} |
Remove a room |
WS |
/overlay/ws/{room} |
Live message stream as JSON |
WebSocket payload format:
{
"type": "message",
"id": "abcd1234",
"room": "chatraszei",
"time": 1745020800.123,
"body": "Hello world https://i.imgur.com/abc.png",
"user": {
"name": "raszei",
"showname": "Raszei",
"isanon": false,
"avatar": "https://fp.chatango.com/profileimg/r/a/raszei/thumb.jpg",
"name_color": "#FF5500",
"font_color": "#FFFFFF"
}
}name_color and font_color are null when the user never picked a colour
(or picked black, which is treated the same since black is unreadable on
stream overlays).
chatango-lib is great but has a few rough edges that StreamChango works
around transparently:
- Colour
zfillbug —utils.py:428doesstr(self._name_color).zfill(6)which turns the 3-char shorthand"FFF"into"000FFF"(almost black, not white). We bypass the broken property and read the raw_name_color/_font_colordirectly, then expand 3 → 6. Client.stop()race — iterates overself.roomswhiledisconnect()mutates it →RuntimeError: dictionary changed size during iteration. We replicate the disconnect loop with a snapshot.- Internal display markers — chatango-lib prefixes anonymous user
names with
!and temporary nicks with#. The official chatango UI never shows these. We strip them before rendering.
The bot doesn't join a room. Check the logs:
journalctl -u streamchango -f. The most common cause is an invalid room
name — the regex is ^[a-z0-9][a-z0-9_-]{0,19}$.
Overlay shows "Connecting..." and never connects. Check your nginx error
log. The most likely cause is missing Upgrade $http_upgrade headers in the
WS location block, or Cloudflare in front of the domain stripping
WebSockets (enable "WebSockets" in your CF zone settings under Network).
A user has no avatar. Anons never have one (by design). Logged-in users who never set a profile picture also won't have one — chatango simply doesn't host an image for them.
Image URL renders as a link instead of an image. The URL must end in a
recognised extension. https://i.imgur.com/abc → link.
https://i.imgur.com/abc.png → image. Sites that hot-link without an
extension (like some .gifv URLs from imgur) will not be recognised.
Two viewers means two bots? No. There's one OverlayBot per process and
each viewer is just a WebSocket subscriber. This is also why --workers 1 is
mandatory in run.sh: a multi-worker setup would mean multiple bots
connecting to the same chatango room.
Messages are duplicated. StreamChango deduplicates by id. If you still
see doubles, please open an issue — chatango-lib may have changed its
protocol.
PRs welcome. Some easy first wins:
- Add more fonts (just add them to the Google Fonts URL in
static/overlay.htmland to theFONTSarray in bothstatic/js/overlay.jsandstatic/config.html) - Per-room overlay presets stored server-side (so streamers don't need to re-paste a URL when they change settings)
- Translation of the panel UI (currently Polish + English mix)
- Native mobile-friendly panel layout
MIT — do whatever you want with it.
chatango-lib (a transitive runtime dependency) is GPL-3.0; we use it
unmodified through pip, so this project remains MIT-licensable.
- neokuze/chatango-lib — async Python client for Chatango that does all the heavy WebSocket work
- giambaJ/jChat — inspiration for the customisation options (size / font / stroke / shadow / animate / fade)
- BotRix — reference for the streamer UX