Overlay MVP for OBS + homelab alerts.
Created from subculture-collective/project-template.
cp .env.example .env
make verify
docker compose up --buildAPI_ADDR/API_PORT— backend listen address and host port.POSTGRES_PORT— host Postgres port. Defaults to55432to avoid clashing with a local Postgres on5432.CORS_ALLOWED_ORIGIN— browser origin allowed to call the API.EXPOSURE_MODE—local,lan, orpublic. Non-local modes make/readyzdegrade whenOPERATOR_SECRETorWEBHOOK_SECRETis empty.OPERATOR_SECRET— optional shared secret for dashboard/operator mutating routes. Send asX-Overlay-Operator-SecretorAuthorization: Bearer <secret>.WEBHOOK_SECRET— optional shared secret forPOST /api/integrations/webhook/{source}. Send asX-Overlay-Webhook-SecretorAuthorization: Bearer <secret>.OVERLAY_STATE_FILE— retained overlay state snapshot path. Defaults to/data/overlay-state.jsonin Docker.EVENT_HISTORY_FILE/EVENT_HISTORY_LIMIT— persisted recent event history path and count limit. Defaults to/data/event-history.jsonland250in Docker.DATABASE_URL— Postgres connection string. When set, event history is stored in Postgresstream_events; when unset, the API uses the JSONL file fallback.- Shared dashboard config is stored in Postgres through operator-protected
/api/dashboard/configwhenDATABASE_URLis set. The browser still keeps a local config cache; secrets remain session-only. - Stream/rehearsal sessions are stored in Postgres through operator-protected
/api/stream/session*endpoints whenDATABASE_URLis set. Active sessions tag new event-history rows withsessionId;/api/stream/session/{id}returns a review report with tagged events and readiness snapshots. PUBLIC_WEB_URL— public base URL for future links and redirects.VITE_API_BASE_URL— optional build-time API base URL for local Vite builds. If unset, overlays derive the API URL from the browser host on port8080.OBS_WEBSOCKET_URL— OBS WebSocket v5 endpoint. Docker defaults tows://host.docker.internal:4455to reach OBS on the host desktop; native API runs can usews://localhost:4455.OBS_WEBSOCKET_PASSWORD— optional OBS WebSocket password when auth is enabled in OBS.TWITCH_CHANNEL— default Twitch channel:patrick__eff.TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,TWITCH_REDIRECT_URL— Twitch OAuth Code flow settings.TWITCH_TOKEN_FILE— durable OAuth token path. Defaults to/data/twitch-token.jsonin Docker.TWITCH_INTERACTIONS_FILE— durable Twitch interaction-rules path. Defaults to/data/twitch-interactions.jsonin Docker.TWITCH_EVENTSUB_ENABLED— starts the Twitch EventSub WebSocket worker when OAuth is configured. Defaults totrue.TWITCH_SCOPES— space-delimited scopes used for the login redirect. Defaults to chat, redemption, subscription, bits/follower, and guarded moderation scopes:user:read:chat user:write:chat channel:read:redemptions channel:manage:redemptions channel:read:subscriptions bits:read moderator:read:followers moderator:manage:chat_messages moderator:manage:banned_users.
Twitch auth routes:
GET /api/auth/twitch/login— start OAuth.POST /api/auth/twitch/login-url— start OAuth from the dashboard withOPERATOR_SECRETas a header and return a Twitch authorize URL.GET /api/auth/twitch/callback— exchange the code and redirect back toPUBLIC_WEB_URL.GET /api/auth/twitch/status— JSON summary of config/auth state and safe diagnostics.
Make sure the Twitch app callback URL matches TWITCH_REDIRECT_URL.
If you run the API in Docker, the Twitch token persists in the api-data volume at /data/twitch-token.json, retained overlay state persists at /data/overlay-state.json, and recent event history persists in Postgres when DATABASE_URL is set. If DATABASE_URL is unset, recent event history falls back to /data/event-history.jsonl.
Twitch interaction rules also persist on api-data at /data/twitch-interactions.json.
OAuth callback test flow:
- Open
http://localhost:5173/dashboard, enterOPERATOR_SECRETif configured, and click Twitch login. IfOPERATOR_SECRETis empty, directhttp://localhost:8080/api/auth/twitch/loginalso works. - Complete Twitch authorization.
- Twitch redirects to
/api/auth/twitch/callback, which stores the token and sends you back toPUBLIC_WEB_URL. - Check
GET /api/auth/twitch/statusforauthenticated,streamerLogin, andtokenExpiresAt.
On failures, the callback redirects with a safe reason query parameter and status includes recent non-secret auth diagnostics.
After OAuth succeeds, the API starts a background EventSub WebSocket worker. It creates subscriptions allowed by the saved token scopes and publishes Twitch events into the existing overlay SSE stream.
Current mappings:
- Twitch chat messages →
chat_messageevents for/overlay/chatand/overlay/ticker. - Channel point redemptions → alert events for
/overlay/alertsand/overlay/ticker. - Subscriptions, resubs, gift subs, bits, follows, and chat notifications → alert events for
/overlay/alertsand/overlay/ticker.
If you added moderator:read:followers after an earlier login, re-run /api/auth/twitch/login so the saved token includes the new follow scope.
The dashboard Config manager includes Twitch interaction rules for backend-side chat aliases and channel point rewards. The API reads and writes them from /data/twitch-interactions.json via TWITCH_INTERACTIONS_FILE.
- Missing
/data/twitch-interactions.jsonis normal on first boot. - A malformed or unreadable rules file degrades
/readyzattwitch_interactionsand disables custom rules until the file is repaired or valid rules are saved again. GET /api/twitch/interactionsandPUT /api/twitch/interactionsare operator-protected.- Chat aliases only resolve to the safe built-in commands
alert,clear,brb, andback. - Channel point reward rules match reward titles case-insensitively and can
alert,brb,clear, orignore.
See docs/runbooks/twitch-interaction-rules.md for the editing, backup, and live-QA flow.
/dashboard/live— canonical recovery console with readiness, recovery runbook links, build info, EventSub diagnostics, setup checklist status, session log, and persisted event history./dashboard— overview, system status, build identity, and route guide for choosing the right runbook or dashboard path./dashboard/setup— setup checklist for OBS onboarding, smoke checks, and setup decisions./dashboard/sources— OBS browser-source URLs, active-profile preset view, and custom CSS copy helpers./dashboard/control— operator quick actions, rehearsals, BRB controls, OBS controls, and session log./dashboard/config— operator secret session storage, automation profiles, Twitch interaction rules, MPRIS decision, and overlay noise controls./version— public, unauthenticated non-secret build/runtime metadata for smoke checks and deploy verification./overlay/alerts/overlay/chat/overlay/goals/overlay/now-playing/overlay/ticker/overlay/brb/overlay/debug
For live operation, open /dashboard/live first during degradation, then use /dashboard to choose the right route or runbook. Follow docs/runbooks/live-stream-preflight.md for quick checks and docs/runbooks/live-qa.md for the full rehearsal/live QA pass.
For bot runtime status, FossaBot-parity audit checks, and guarded moderation/nuke operations, follow docs/runbooks/bot-ops.md.
For Twitch interaction rules, follow docs/runbooks/twitch-interaction-rules.md.
For production recovery, follow docs/runbooks/production-recovery.md.
For persisted recent event history and recovery guidance, follow docs/runbooks/event-history.md.
For OBS automation sequences, follow docs/runbooks/obs-automation-sequences.md.
For moderator or broadcaster Twitch chat controls, use docs/runbooks/twitch-chat-commands.md.
For post-build route checks and styling changes, use docs/runbooks/visual-qa.md.
/healthz stays the cheap liveness check used by Compose healthchecks. curl http://localhost:8080/readyz is the operator diagnostics endpoint and may return status: degraded when OBS, Twitch, EventSub, retained state, event history, or Twitch interaction rules need attention.
Open the operator dashboard at:
http://<homelab-host>:5173/dashboard
The dashboard can check Twitch auth status, open the Twitch login flow, discover OBS inputs and scene items, send test alerts, clear overlays, control the BRB/intermission screen, and send generic webhook test events. It also includes an OBS setup workflow with copyable browser-source URLs, stream preset guidance, one-click sample events for alert/ticker/goals/BRB, and OBS diagnostics hints.
If OPERATOR_SECRET is configured, enter it in the dashboard before using mutating controls such as test alerts, clear, BRB, or OBS commands.
The same secret is required to start Twitch OAuth; the dashboard sends it as a header to request a one-time Twitch authorize URL.
It can also connect to OBS WebSocket to list scenes, switch the current program scene, mute/unmute an input, and show/hide scene items by ID.
Automation profiles are stored locally in /dashboard/config. The active profile is respected by /dashboard/control and /dashboard/sources, which means the operator can switch between show modes without deleting unused actions or presets. Export/import on /dashboard/config backs up the dashboard config JSON only; it does not include the operator secret or other session-only data.
OBS WebSocket notes:
- Enable OBS WebSocket in OBS:
Tools → WebSocket Server Settings. - Docker default OBS WebSocket v5 URL is
ws://host.docker.internal:4455, with a Composehost-gatewaymapping for Linux. - Native API runs can use
ws://localhost:4455. - If Docker cannot reach OBS through the bridge network, use a local
docker-compose.override.ymlwith API host networking andOBS_WEBSOCKET_URL=ws://localhost:4455. - If API host networking is enabled, set
DATABASE_URLto the host-published Postgres port, for examplepostgres://<user>:<password>@localhost:55432/<db>?sslmode=disable, because the Compose DNS namepostgresis not available from the host network namespace. - If OBS runs on another machine, set
OBS_WEBSOCKET_URL=ws://<obs-host>:4455for the API container/host. - If OBS WebSocket auth is enabled, set
OBS_WEBSOCKET_PASSWORDin.env.
Use this for the main alert source:
http://<homelab-host>:5173/overlay/alerts
The Docker web image supports direct overlay URLs with an nginx SPA fallback, so OBS can open /overlay/alerts directly. By default, the browser source connects to http://<same-host>:8080/api/events/stream. If your API is on a different host or port, add an override query parameter:
http://<homelab-host>:5173/overlay/alerts?apiBaseUrl=http://<api-host>:8080
Recommended settings:
- Width:
1920 - Height:
1080 - Custom CSS: empty to start
- Shutdown source when not visible: off
- Refresh browser when scene becomes active: off
For the other browser sources, point OBS at the matching route above.
Local dev:
- Use
cp .env.example .env, keepOPERATOR_SECRET=empty while developing, and runmake devplus the API ordocker compose up --build. - Direct route access such as
http://localhost:5173/dashboardandhttp://localhost:5173/overlay/alertsis expected.
LAN-only streaming:
- Set a non-empty
OPERATOR_SECRETbefore using the dashboard on your network. - Set
EXPOSURE_MODE=lanso/readyzwarns if operator/webhook secrets are empty. - Keep overlay browser-source routes readable for OBS, but treat API port
8080as a trusted-LAN service. - Set
CORS_ALLOWED_ORIGINto the dashboard/web origin instead of*when the host name is stable. - Use
docker-compose.override.example.ymlas a template when you need host MPRIS session-bus access.
Public reverse proxy:
- Put
/dashboardand API mutating routes behind reverse-proxy authentication in addition toOPERATOR_SECRET. - Set
EXPOSURE_MODE=public,OPERATOR_SECRET, andWEBHOOK_SECRETbefore exposing any API route. - OBS scene/input inventory endpoints are operator-protected because they reveal local OBS structure.
- Expose only the overlay routes needed by OBS/browser sources when possible.
- Do not publish the API directly to the internet without auth/rate-limit/reverse-proxy controls.
- Keep
TWITCH_TOKEN_FILE,OVERLAY_STATE_FILE, andEVENT_HISTORY_FILEon theapi-datavolume and include that volume in backups if you want OAuth/state continuity and recent event history.
Use docs/runbooks/production-packaging.md for the full packaging, smoke, backup, and restore flow.
/versionreturnsname,version,commit,builtAt,goVersion,os, andarchwithout exposing secrets.- Local
make buildusesVERSION,BUILD_COMMIT, andBUILD_DATE;make docker-buildpasses those values as DockerBUILD_VERSION,BUILD_COMMIT, andBUILD_DATE. Plaindocker compose up --buildusesBUILD_VERSION,BUILD_COMMIT, andBUILD_DATEfrom the shell or.env, defaulting todev/unknown/unknown. - Run
make postgres-backupto snapshot Postgres andmake api-data-backupto snapshot the Composeapi-datavolume. - Backup artifacts can contain Twitch tokens, retained state, event history, and Twitch interaction rules, so keep
BACKUP_DIRprivate and ignored. - Restore with the API stopped before unpacking a backup back into
api-data.
Use this for chat:
http://<homelab-host>:5173/overlay/chat
/overlay/ticker listens to the full SSE stream and turns recent alerts, chat, service status, and custom commands into a lower-third ticker. To control ticker copy explicitly, include data.ticker in a webhook payload:
{"title":"Schedule","message":"Stream starts at 19:00","data":{"ticker":{"label":"schedule","message":"Stream starts at 19:00"}}}The most recent ticker payload is retained and replayed to OBS browser sources after reload/reconnect.
/overlay/goals listens for explicit data.goal or data.goals payloads. Example goal update:
{"title":"Follower goal","message":"next unlock at 700","data":{"goal":{"id":"followers","label":"Follower goal","current":430,"target":620,"unit":"followers","note":"next unlock at 700"}}}The most recent goal payload is retained and replayed to OBS browser sources after reload/reconnect.
/overlay/now-playing listens for backend now_playing events from Linux MPRIS. Enable it on a host that can access your user session D-Bus:
MPRIS_ENABLED=true
MPRIS_PLAYER= # optional exact pin; accepts Feishin or org.mpris.MediaPlayer2.Feishin
MPRIS_PLAYER_PRIORITY=Feishin,mpv,cliamp,ferrosonic,playerctld,vivaldi
MPRIS_POLL_INTERVAL=2sIf MPRIS_PLAYER is empty, over-laid scans org.mpris.MediaPlayer2.*, chooses a Playing player first, then Paused, then Stopped, and uses MPRIS_PLAYER_PRIORITY to break ties among players with the same playback state. Priority entries accept short names or full bus names. Set MPRIS_PLAYER=Feishin only when you want to pin one app and ignore the rest.
MPRIS runs on the user session bus. Native API runs are the simplest supported path. Docker MPRIS is advanced: the base compose file only passes DBUS_SESSION_BUS_ADDRESS; you must also run as the matching host user and mount the host bus socket with an override, for example:
services:
api:
user: "1000:1000"
environment:
DBUS_SESSION_BUS_ADDRESS: unix:path=/run/user/1000/bus
volumes:
- /run/user/1000/bus:/run/user/1000/bus/overlay/brb listens for dashboard brb events. Use the dashboard Intermission controls, or send a direct event:
curl -X POST http://localhost:8080/api/events/brb \
-H 'Content-Type: application/json' \
-H 'X-Overlay-Operator-Secret: <OPERATOR_SECRET if configured>' \
-d '{"active":true,"title":"Be right back","message":"Short intermission. Stream resumes soon.","eta":"5 min","items":["Hydrate","Stretch","Chat stays live"]}'Hide the BRB screen without clearing every overlay:
curl -X POST http://localhost:8080/api/events/brb \
-H 'Content-Type: application/json' \
-H 'X-Overlay-Operator-Secret: <OPERATOR_SECRET if configured>' \
-d '{"active":false,"title":"BRB overlay","message":"Waiting for intermission command"}'The current BRB/intermission state is retained and replayed to OBS browser sources after reload/reconnect. POST /api/events/clear clears retained overlay state.
Send a test alert:
curl -X POST http://localhost:8080/api/events/test-alert \
-H 'Content-Type: application/json' \
-H 'X-Overlay-Operator-Secret: <OPERATOR_SECRET if configured>' \
-d '{"title":"Test alert","message":"Overlay pipeline is live","severity":"success"}'Clear the overlay:
curl -X POST http://localhost:8080/api/events/clear \
-H 'X-Overlay-Operator-Secret: <OPERATOR_SECRET if configured>'Send a generic webhook event:
curl -X POST http://localhost:8080/api/integrations/webhook/gitea \
-H 'Content-Type: application/json' \
-H 'X-Overlay-Webhook-Secret: <WEBHOOK_SECRET if configured>' \
-d '{"title":"Gitea","message":"Push received","severity":"info"}'Examples for the future webhook adapters:
curl -X POST http://localhost:8080/api/integrations/webhook/n8n \
-H 'Content-Type: application/json' \
-d '{"title":"n8n","message":"Workflow ran","severity":"success"}'
curl -X POST http://localhost:8080/api/integrations/webhook/freshrss \
-H 'Content-Type: application/json' \
-d '{"title":"FreshRSS","message":"New article","severity":"info"}'
curl -X POST http://localhost:8080/api/integrations/webhook/ntfy \
-H 'Content-Type: application/json' \
-d '{"title":"ntfy","message":"Notification received","severity":"warning"}'This repository keeps the base Open Pilot issue template and PR template. Bootstrap labels for this repo with:
open-pilot labels bootstrap PatrickFanella/over-laidGPL-3.0-only. See LICENSE.