One chat overlay for Twitch, Kick, YouTube and TikTok.
BepChat merges live chat from every platform you stream on into a single transparent page you can drop into OBS as a browser source. It renders 7TV, BetterTTV and FrankerFaceZ emotes on all four platforms, shows real badge art and each service's own logo beside its messages, and supports 7TV name paints.
There is no account system. All configuration lives in the overlay URL, so the public instance stores nothing about you and a self-hosted instance needs no database.
Build an overlay URL at chat.dek.to, then add it in OBS under Sources → + → Browser. The background is transparent, so no chroma key is needed.
| Platform | Transport | Credentials |
|---|---|---|
| Twitch | Anonymous IRC over WebSocket, direct from the browser | None |
| Kick | Kick's public Pusher feed, direct from the browser | None |
| YouTube | Relayed by the server over SSE, using YouTube's internal live chat API | None |
| TikTok | Relayed by the server via tiktok-live-connector |
None, but see below |
Twitch and Kick never touch the server, which is why they scale for free. Only YouTube and TikTok need a relay, and identical channels share one upstream connection no matter how many viewers are watching.
git clone https://github.com/dekrom/BepChat && cd BepChat
cp .env.example .env # optional; every value has a working default
docker compose up -dOpen http://localhost:8080.
Requires Node.js 22 or newer.
npm install
npm startTo skip TikTok support and run with zero dependencies:
npm install --omit=optional
npm startEvery variable is optional. An instance with no configuration at all still serves working overlays for all four platforms — the settings below only affect badge art, capacity and TikTok quota.
| Variable | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP port |
HOST |
0.0.0.0 |
Bind address |
TWITCH_CLIENT_ID |
– | Optional app credentials from dev.twitch.tv. Badge art works without them; setting them routes global badges through the official Helix API instead. |
TWITCH_CLIENT_SECRET |
– | As above |
EULER_API_KEY |
– | EulerStream key raising the TikTok signing quota |
TIKTOK_ENABLED |
true |
Set false to disable TikTok relaying entirely |
MAX_ROOMS |
50 |
Concurrent upstream relay connections |
MAX_CLIENTS |
300 |
Concurrent overlay viewers on the relay |
MAX_CLIENTS_PER_IP |
5 |
Per-address relay connection limit |
RATE_LIMIT_PER_MIN |
30 |
Requests per minute for the JSON endpoints |
ROOM_IDLE_GRACE_SEC |
60 |
How long an unwatched relay stays open, so an OBS refresh does not reconnect upstream |
TRUST_PROXY |
– | Set 1 when behind a reverse proxy so X-Forwarded-For is honoured |
YT_OFFLINE_POLL_SEC |
60 |
How often to check whether a YouTube channel went live |
YT_POLL_MAX_MS |
2500 |
Gap between live-chat polls; YouTube asks for 10s, which leaves its messages that far behind Twitch and Kick |
/overlay?twitch=yourname&kick=yourname&size=2&font=6&fade=30&stroke=1
| Parameter | Value |
|---|---|
twitch |
Channel login name |
kick |
Channel slug |
youtube |
@handle or UC… channel id |
tiktok |
Username, with or without the @ |
ytv |
A specific YouTube video id. Only needed for unlisted broadcasts; public streams are discovered automatically. |
kick_room |
Numeric Kick chatroom id. Only needed if your network blocks Kick's channel API. |
| Parameter | Values | Default | Effect |
|---|---|---|---|
size |
1–3 |
2 |
Text size |
font |
0–15 |
0 |
Font family — see below |
weight |
300–900 |
600 |
Font weight; names render 200 heavier, capped at 900 |
italic |
true/false |
false |
Italic text |
uppercase |
true/false |
false |
Uppercase names |
entry |
slide/fade/pop/grow/none |
slide |
Entry animation |
fade |
seconds | 0 |
Remove messages after N seconds; 0 keeps them |
stroke |
0–3 |
0 |
Text outline width |
stroke_color |
six hex digits | 000000 |
Outline colour |
shadow |
0–3 |
2 |
Text shadow strength |
shadow_color |
six hex digits | 000000 |
Shadow colour |
glow |
0–3 |
0 |
Coloured glow layered over the shadow |
glow_color |
six hex digits | ffffff |
Glow colour |
spacing |
0–24 |
4 |
Pixels between messages |
max |
1–200 |
40 |
Messages kept on screen |
bg |
none/dark/light |
none |
Message background box |
bg_opacity |
0–100 |
60 |
Box opacity when bg is set |
direction |
up/down |
up |
Whether new messages appear at the bottom or the top |
timestamps |
true/false |
false |
Show HH:MM before each message |
chips |
icon/color/none |
icon |
Platform mark: the service's logo, a coloured square, or nothing |
badges |
true/false |
true |
Show badges |
name_color |
six hex digits | – | Force one colour for every name. 7TV paints still take precedence. |
animate |
true/false |
true |
Legacy alias: false means entry=none |
| id | Font | id | Font | |
|---|---|---|---|---|
0 |
System | 8 |
SF Pro (Apple) | |
1 |
Arial Black | 9 |
SF Pro Rounded | |
2 |
Verdana | 10 |
Segoe UI | |
3 |
Comic Sans | 11 |
Tahoma | |
4 |
Trebuchet | 12 |
Times | |
5 |
Courier | 13 |
Century Gothic | |
6 |
Impact | 14 |
Consolas | |
7 |
Georgia | 15 |
Palatino |
SF Pro ships with macOS and iOS and cannot be redistributed as a webfont, so
ids 8 and 9 only render as SF Pro where the font is installed. Everywhere
else they fall back to the closest available face. This matters for OBS: the
font has to exist on the machine running the browser source, not on the server.
Ids 0–7 are fixed and will not be renumbered.
| Parameter | Values | Default | Effect |
|---|---|---|---|
platforms |
csv of twitch,kick,youtube,tiktok |
all | Only connect to these platforms |
hide_commands |
true/false |
false |
Hide messages starting with ! |
hide_bots |
csv of names | – | Hide specific chatters, e.g. nightbot,streamelements |
These are properties of the platforms, not bugs:
- YouTube has no public live chat API, so BepChat uses the same internal endpoint the web player does. It can break when YouTube changes their markup.
- YouTube unlisted streams are invisible to automatic discovery. Use
ytv. - TikTok requires a third-party service (EulerStream) to sign its WebSocket
handshake. The free tier is limited, so on a busy public instance TikTok is
best-effort — the overlay reports
throttledrather than failing silently. SetEULER_API_KEYfor a higher quota. - Kick sits behind Cloudflare, which rejects server-side requests based on
their TLS fingerprint. BepChat resolves Kick channels in the browser instead,
where it works.
kick_roomis the manual fallback. - Twitch badge art and channel emote sets need the channel's numeric id,
which the official Helix API only hands out to a registered app. So that a
zero-configuration instance still works, BepChat reads the same public data
from the endpoint twitch.tv's own web client uses. Setting
TWITCH_CLIENT_ID/SECRETswitches global badges to Helix. - YouTube Super Chats and membership posts are rendered, the former with its amount shown as a badge. Subs, raids, gifts and moderation events on every platform are ignored by design.
npm install
npm test # node --test, no test framework needed
npm startThere is no build step. The server is plain Node ESM and the frontend is native ES modules, so editing a file and reloading is the whole workflow.
src/ HTTP server, relay rooms, platform sources, upstream API clients
public/ overlay page, builder page, both as native ES modules
shared/ identifier validation used by the server and the browser alike
test/ node:test suites
Badge and emote artwork belongs to its respective owners: Twitch, Kick, YouTube,
TikTok, 7TV, BetterTTV and
FrankerFaceZ. Kick's standard badge SVGs are vendored
in public/kickbadges/, and each platform's logo is drawn in
public/overlay/icons.mjs to identify which chat a message came from. Those
marks are the trademarks of their respective owners. BepChat is not affiliated
with, endorsed by, or sponsored by any of these services.
MIT — see LICENSE.