How TriBathon works: one live timer fed by four streaming platforms
The architecture behind TriBathon, a subathon timer for streamers: how events from Twitch, Kick, YouTube and StreamElements become seconds on a live OBS overlay, on a single VPS.
- architecture
- redis
- webhooks
- side-project
TriBathon is a subathon timer for streamers. During a subathon, every sub, gift, donation or follow adds time to a countdown shown on stream, and the stream lasts until the timer hits zero. The hard part is that a streamer’s audience is spread over several platforms at once, and each platform delivers events in a completely different way. TriBathon merges them into one timer that updates on stream in near real time.
I built and run it alone. This note covers the shape of the system, not the parts that would help someone attack it.
The pieces
| Piece | Technology | Job |
|---|---|---|
| Website and dashboard | Astro, React, Tailwind | Static marketing pages in English and Spanish; the dashboard is a React app |
| API | Node.js, Express, TypeScript | Accounts, settings, platform connections, webhooks, billing |
| Worker | Node.js, Bull queues | Processes platform events and scheduled jobs |
| State | PostgreSQL, Redis | Postgres is the source of truth; Redis holds hot timer state, queues and dedup keys |
| Realtime | Socket.IO with a Redis adapter | Pushes timer updates to the on-stream overlay |
| Billing | Paddle | Subscriptions and payments |
All of it runs as Docker Compose services on one VPS behind a reverse proxy. There is no Kubernetes here, on purpose: I run enough clusters at work, and one well-monitored machine with backups is the right size for this product today.
Four platforms, four ways of saying “something happened”
Each platform has its own delivery model, and the integration layer exists to hide that:
- Twitch pushes events to the API as signed webhooks (EventSub).
- Kick also uses signed webhooks, with a different signing scheme.
- YouTube has no webhooks for live chat. TriBathon keeps a streaming connection open to the live chat API while the streamer is live, and uses push notifications to know when a stream starts. YouTube’s API quota is tight, so usage is tracked continuously.
- StreamElements (donations) delivers events over a persistent WebSocket that TriBathon holds open on the streamer’s behalf.
Whatever the source, every event is normalised into the same internal shape and goes down the same path. Platform-specific code stops at the edge.
From event to seconds on screen
- Accept fast. A webhook request is verified, deduplicated, put on a queue, and answered immediately. Platforms retry or disable webhooks that respond slowly, so no real work happens inside the request.
- Deduplicate. Platforms retry, and sometimes deliver the same event twice. Each event ID is claimed atomically in Redis before it is processed. If Redis is unavailable, the API deliberately answers with an error so the platform retries later, rather than risk crediting the same sub twice. Losing a second of time is better than giving a streamer time they did not earn, and there are tests that specifically try to double-credit events.
- Apply the streamer’s rules. A worker computes how much time the event is worth: per-platform values, thresholds, happy-hour multipliers and caps are all configured by the streamer.
- Update the timer. The live timer state sits in Redis and is written back to Postgres in batches, so a busy raid does not turn into hundreds of database writes per second. If the cache misbehaves, a circuit breaker falls back to the database.
- Push to the overlay. The overlay is a browser source in OBS. It connects with a per-streamer widget token, joins that streamer’s room, and receives updates as they happen. The Redis adapter lets any API instance publish to any overlay.
Billing
Paddle handles payments. Its webhooks are signature-checked and drive the subscription lifecycle: created, updated, past due, cancelled. Because webhooks can be missed, a scheduled job reconciles subscription state with Paddle regularly instead of trusting the event stream alone. Trial reminders and abandoned-checkout follow-ups run as jobs in the same worker.
Operations
This is where my day job shows up most:
- Background jobs run as repeatable queue jobs in the worker: refreshing platform tokens before they expire, purging and anonymising old data, reconciliation, recaps.
- Monitoring that knows the domain. Generic uptime checks miss the failure that matters here: the service is up but events stopped arriving. There are monitors for stalled queues and for platforms that suddenly go silent during live streams, and they alert me directly.
- Logs are structured and redact sensitive fields, with a test that checks the redaction.
- Deploys are a script: pull, back up the database, build, run migrations, restart. Boring on purpose.
- CI runs type checks plus unit and integration tests on every push, and end-to-end tests cover the main flows.
Where AI fits
Most of the code was written with AI coding assistants, with me owning the architecture, reviewing every change and operating it in production. Inside the product, AI is used in one internal place: a Discord support assistant that diagnoses a ticket from a sanitised, allowlisted evidence pack and drafts a reply that only I can send. How that assistant is fenced in has its own note. Nothing a streamer sees on stream depends on a language model.