Real-time stream monitoring API for Discord bots.
Detects when streamers go live across 3 platforms and delivers instant alerts to Discord channels.
Supported Platforms
Features | Quick Start | Deploy | API Docs | Fork Guide
| Feature | Description |
|---|---|
| 3 Platforms | Twitch, YouTube, TikTok |
| Smart Scheduler | Dynamic intervals based on streamer activity |
| Dual Storage | JSON files (dev) / Prisma MySQL (prod) |
| Idempotent Alerts | Restarts never cause duplicate notifications |
| Unified Whitelist | Single WHITELIST env controls IP + CORS |
| Per-Guild Config | Each Discord server picks its channel and mention role |
| Anti Rate-Limit | UA rotation, random delays, exponential backoff |
| Rate Limited API | 60 req/min per IP |
+----------------+ +----------------+ +----------------+
| Scheduler |--->| Scraper |--->| Storage |
| (30s tick) | | (per platform) | | (JSON/Prisma) |
+----------------+ +----------------+ +-------+--------+
|
+----------------+ +----------------+ |
| Discord Bot |<---| REST API |<-----------+
| (consumer) | | /events |
+----------------+ +----------------+
- Scheduler ticks every 30s, picks streamers due for a check
- Scraper checks live status on the platform
- On offline -> live transition, a
StreamEventis created - Discord Bot polls
GET /events?since=...for new events - Bot sends an embed to the Discord channel, then calls
PATCH /events/:id/notify { guildId } - Next poll skips already-notified guilds (persistent, restart-safe)
| Scenario | Alert? | Reason |
|---|---|---|
| Offline -> Live | Yes (1x) | New transition detected |
| Still live (hours later) | No | No new transition |
| Live -> Offline | No | Not a live event |
| Offline -> Live again (new session) | Yes (1x) | New transition, new event |
| Service restarts while live | No | notifiedGuilds persisted in storage |
+-------------------------------------------------------------+
| Gateway: Helmet, CORS, Rate Limit |
+----------------------+--------------------------------------+
|
+----------------------v--------------------------------------+
| Security: IP Whitelist, Validation |
+----------------------+--------------------------------------+
|
+----------------------v--------------------------------------+
| Routes (/streamers, /events, /subscriptions) |
| Controllers (CRUD + markNotified) |
+-----------------------------+-------------------------------+
|
+-----------------------------v-------------------------------+
| Storage: JsonStorage (dev) | PrismaStorage (prod) |
+-----------------------------+-------------------------------+
|
+-----------------------------v-------------------------------+
| Scheduler + Scrapers: Twitch, YouTube, TikTok |
+-------------------------------------------------------------+
Development uses JSON file storage. No database needed.
# Clone
git clone https://github.com/lrmn7/mewwme-stream-alert.git
cd mewwme-stream-alert
# Install
npm install
# Configure
echo "NODE_ENV=development" > .env
echo "PORT=3005" >> .env
echo "DISABLE_IP_WHITELIST=true" >> .env
# Run
npm run devServer starts at http://localhost:3005. Done.
npm install
npx prisma generate
npx prisma db push # first time only
npm run build
npm startRequired .env:
NODE_ENV=production
PORT=3005
DATABASE_URL="mysql://user:pass@host:3306/stream_monitor"
WHITELIST=your-bot-ip,your-frontend.com
DISABLE_IP_WHITELIST=false- Connect your GitHub repo (
lrmn7/mewwme-stream-alert) - Set root directory if monorepo
- Add env vars in dashboard (see table below)
- Start command:
npm run build && npm start
- Create a Web Service, connect your repo
- Build:
npm install && npx prisma generate && npm run build - Start:
npm start - Add env vars in dashboard
Create vercel.json:
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"rewrites": [{ "source": "/(.*)", "destination": "/index.js" }]
}For the full service (scheduler + scraper + API), use Railway, Render, or a VPS.
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV |
No | development |
development = JSON, production = Prisma |
PORT |
No | 3005 |
Server port |
DATABASE_URL |
Prod | - | MySQL connection string |
DATABASE_PROVIDER |
No | mysql |
mysql or postgresql |
WHITELIST |
Prod | - | IPs + domains, comma-separated |
DISABLE_IP_WHITELIST |
No | false |
true to disable (dev only) |
LOG_LEVEL |
No | info |
debug / info / warn / error |
One env var controls both IP whitelist and CORS:
WHITELIST=1.2.3.4,your-frontend.com,http://localhost:3000- Starts with digit or contains
:-> IP whitelist - Otherwise -> CORS origin (auto-prefixed
https://if no scheme) 127.0.0.1,::1always allowed- Dev mode auto-allows
localhost:3000andlocalhost:5173
GET /health{ "status": "ok", "uptime": 3600, "scheduler": true, "timestamp": "..." }POST /streamers # Create (body: platform, username, userId)
GET /streamers?platform=twitch&isLive=true # List (all params optional)
GET /streamers/:id # Detail (includes events + subscriptions)
DELETE /streamers/:id # Delete (cascades events + subscriptions)Example: Create Streamer
Request:
{ "platform": "twitch", "username": "mewwme", "userId": "default" }Response: 201
{
"streamer": {
"id": "dev_abc123",
"platform": "twitch",
"username": "mewwme",
"displayName": "mewwme",
"isLive": false,
"createdAt": "2026-03-31T12:00:00.000Z"
}
}GET /events?since=ISO&limit=50&streamerId=x # List events
PATCH /events/:id/notify # Mark notified (body: { guildId })Example: List Events
{
"events": [{
"id": "evt_abc123",
"streamerId": "str_abc123",
"title": "Playing Valorant",
"thumbnail": "https://...",
"profileImage": "https://...",
"url": "https://twitch.tv/mewwme",
"notifiedGuilds": ["guild_123"],
"startedAt": "2026-03-31T10:00:00.000Z",
"createdAt": "2026-03-31T10:00:05.000Z",
"streamer": { "id": "str_abc123", "platform": "twitch", "username": "mewwme", "displayName": "mewwme" }
}],
"count": 1
}POST /subscriptions # Create (body: streamerId, guildId, channelId, mentionRoleId?)
GET /subscriptions?guildId=x # List by guild
GET /subscriptions?streamerId=x # List by streamer
DELETE /subscriptions/:id # Delete| Layer | Details |
|---|---|
| Helmet | Standard security headers |
| CORS | Only WHITELIST domains allowed |
| IP Whitelist | All routes protected, only whitelisted IPs |
| Rate Limit | 60 req/min per IP |
| Validation | All inputs validated before processing |
Tick: every 30 seconds. Processes streamers in batches of 10 with 3s delay between batches.
Dynamic intervals (production):
| Last Live | Interval | Reason |
|---|---|---|
| < 1 day | 5 min | Likely to stream again |
| < 7 days | 10 min | Active |
| < 30 days | 20 min | Semi-active |
| 30+ days | 30 min | Inactive |
Development mode: all streamers checked every 60 seconds.
| Platform | Method | Data |
|---|---|---|
| Twitch | GraphQL API | Title, viewers, game, thumbnail |
| YouTube | HTML (ytInitialData) |
Title, viewers, thumbnail |
| TikTok | HTML (SIGI_STATE) |
Title, viewers, avatar |
Anti rate-limit: UA rotation (12+ strings), 500-1500ms random delay, 3 retries with backoff.
| JsonStorage (dev) | PrismaStorage (prod) | |
|---|---|---|
| Backend | data/*.json files |
MySQL via Prisma ORM |
| Database needed | No | Yes |
| Restart | Resets isLive=false |
No reset |
| Concurrency | Not safe | ACID-compliant |
| Indexing | None | streamerId, createdAt |
IStorage Interface
interface IStorage {
createStreamer(data): Promise<StoredStreamer>
findAllActiveStreamers(): Promise<StoredStreamer[]>
updateStreamer(id, data): Promise<void>
deleteStreamer(id): Promise<void>
createEvent(data): Promise<StoredStreamEvent>
findEvents(filters): Promise<EventWithStreamer[]>
markEventNotified(eventId, guildId): Promise<void>
createSubscription(data): Promise<StoredSubscription>
findSubscriptionsByStreamer(streamerId): Promise<StoredSubscription[]>
findSubscriptionsByGuild(guildId): Promise<StoredSubscription[]>
deleteSubscription(id): Promise<void>
}mewwme-stream-alert/
+-- prisma/schema.prisma
+-- data/ # dev-only JSON storage
+-- src/
| +-- index.ts # entry point
| +-- server.ts # Express setup
| +-- controllers/
| | +-- streamerController.ts
| | +-- eventController.ts
| | +-- subscriptionController.ts
| +-- routes/
| | +-- streamers.ts
| | +-- events.ts
| | +-- subscriptions.ts
| +-- middleware/
| | +-- ipWhitelist.ts # IP + CORS whitelist
| | +-- validate.ts
| +-- services/
| | +-- scheduler.ts
| | +-- scraper/ # twitch, youtube, tiktok
| | +-- storage/ # interface, jsonStorage, prismaStorage
| +-- utils/logger.ts
+-- tests/
+-- .env
+-- package.json
+-- tsconfig.json
| Development | Production | |
|---|---|---|
| Storage | JSON (data/*.json) |
Prisma (MySQL) |
| Database | Not needed | Required |
| Check interval | 60s | 5-30 min (dynamic) |
| IP Whitelist | Disabled | Required |
| Restart | Resets isLive |
No reset |
| CORS | Auto-allows localhost | Only WHITELIST |
npm test # all tests
npm run test:api # API endpoints
npm run test:security # security middleware
npm run test:scraper # scrapers
npm run test:scheduler # scheduler logic
npm run test:watch # watch mode
npm run typecheck # tsc --noEmitgit clone https://github.com/lrmn7/mewwme-stream-alert.git
cd mewwme-stream-alert
npm installNODE_ENV=development
PORT=3005
DISABLE_IP_WHITELIST=truenpm run dev # dev (JSON, hot-reload)
npm run build # compile
npm start # productionCreate src/services/scraper/newplatform.ts:
export async function checkNewPlatform(username: string): Promise<CheckResult> {
return {
isLive: true,
title: "Stream title",
thumbnail: "https://...",
profileImage: "https://...",
url: "https://newplatform.com/" + username,
viewers: 0,
};
}Register in src/services/scraper/index.ts.
npx prisma generate
npx prisma db push # create tables
npx prisma studio # browse dataSee LICENSE for details.