Hosted, multi-tenant version of clip-bot. Streamers log in with Twitch, optionally link YouTube (Ultra), invite one shared Discord bot to their server(s), and pay via Stripe for Pro or Ultra. You run this as a single server; each active streamer gets their own monitoring under the hood, but they don't have to run or configure anything themselves.
PayPal was the original plan, but it turned out not to be available for this deployment: PayPal Subscriptions requires "Reference Transactions" approval on your PayPal Business account (a manual support request, not self-service), and PayPal-processed-through-Stripe is only available to Stripe accounts based in the EU/UK/Switzerland/Norway — not Canada. So billing runs on Stripe Checkout + Billing (cards) instead.
There are now two paid plans, so you need two Stripe Prices:
- A Product with two recurring Prices. In the
Stripe dashboard, go to Product catalog
→ Add product. Add a recurring monthly Price for Pro ($2.99/mo) and
another for Ultra ($5.99/mo) — either as two Prices on one Product or
two separate Products, doesn't matter functionally. Copy each Price
ID (
price_XXXXXXXXXXXXXX) intoSTRIPE_PRO_PRICE_IDandSTRIPE_ULTRA_PRICE_ID. - API keys. Developers → API keys. Use Test mode keys while developing, switch to live when ready for real payments.
- A webhook. Developers → Webhooks → Add endpoint:
{BASE_URL}/webhooks/stripe, subscribed to at least:checkout.session.completed,customer.subscription.updated,customer.subscription.deleted. Copy the Signing secret (whsec_...) intoSTRIPE_WEBHOOK_SECRET.
How a checkout links back to the right plan: GET /billing/subscribe?plan=pro
(or ?plan=ultra) stamps the plan name into both the Checkout session's
metadata and the resulting subscription's metadata (src/billing/stripe.js).
The webhook reads that back (src/routes/webhooks.js) rather than assuming
everyone is on Pro — important, otherwise an Ultra renewal event would
silently downgrade someone to Pro-level access.
Test everything with Stripe's Test mode first — test card
4242 4242 4242 4242, any future expiry, any CVC.
src/
├── config.js env loading/validation
├── db.js SQLite schema + query helpers + migrations
├── spikeDetector.js chat-velocity spike detection, shared by Twitch + YouTube
├── messageTemplates.js {clip_url} template rendering + defaults
├── tenantManager.js starts/stops monitoring per active streamer, routes spikes to chat/Discord
├── twitch/
│ ├── oauth.js Twitch login (Authorization Code flow)
│ ├── tenantAuth.js per-user token cache + refresh-on-401
│ ├── eventsubManager.js multi-tenant EventSub WebSocket client (push-based)
│ ├── clipCreator.js Create Clip API + polling, per-tenant
│ └── chatSender.js posts into the streamer's own Twitch chat (Free tier)
├── youtube/ Ultra tier only
│ ├── oauth.js Google OAuth (separate app/credentials from Twitch)
│ ├── tenantAuth.js per-user token cache + refresh, mirrors twitch/tenantAuth.js
│ ├── liveMonitor.js polling lifecycle manager (YouTube has no push events)
│ └── chatSender.js posts into the streamer's own YouTube live chat
├── discord/
│ └── discordBot.js one shared bot login, posts into any tenant's channel(s)
├── billing/
│ └── stripe.js Stripe client: Checkout sessions (2 plans), webhook verification
├── routes/
│ ├── auth.js /auth/twitch, /auth/twitch/callback, /logout
│ ├── youtubeAuth.js /auth/youtube, /auth/youtube/callback, /auth/youtube/unlink (Ultra-gated)
│ ├── dashboard.js /dashboard (+ /twitch, /discord, /youtube, /clips subpages, each with its own GET + POST)
│ ├── billing.js /billing/subscribe?plan=, /billing/success, /billing/cancel
│ └── webhooks.js /webhooks/stripe
├── public/
│ └── styles.css shared stylesheet for all views
└── views/
├── landing.ejs
├── partials/ dashboardHead.ejs, dashboardNav.ejs, dashboardFoot.ejs — shared shell + tab nav
└── dashboard/ overview.ejs, twitch.ejs, discord.ejs, youtube.ejs, clips.ejs — one page per settings area
Originally this was a single /dashboard page with every setting stacked on
it. It's now split so each platform/area has its own page and its own save
button, sharing a tab strip (views/partials/dashboardNav.ejs):
/dashboard— plan status, upgrade/cancel, GrowLive Discovery opt-in/dashboard/twitch— chat message template, spike sensitivity/dashboard/discord— Discord message template, auto-post toggle, channel list/dashboard/youtube— YouTube connect/unlink (Ultra)/dashboard/clips— clip history with per-clip content-type tagging
Each page posts to its own route (POST /dashboard/twitch, POST /dashboard/discord, etc.) and only updates the columns that page owns —
db.js has updateTwitchSettings/updateDiscordSettings/
updateDiscoverySettings instead of one combined updateUserSettings,
specifically so saving one page can't blank out settings that live on a
different page.
npm install
cp .env.example .envFill in .env:
- Twitch: create an app at https://dev.twitch.tv/console/apps with
redirect URI
{BASE_URL}/auth/twitch/callback. Paste client id/secret. - Google/YouTube (Ultra only): create an OAuth client at
console.cloud.google.com with redirect URI
{BASE_URL}/auth/youtube/callback, enable the YouTube Data API v3 on that project. This is a genuinely separate setup from Twitch — different console, different app. - Discord: create a bot at https://discord.com/developers/applications,
copy its token into
DISCORD_BOT_TOKEN. Generate an OAuth2 invite URL (scopebot, permissions: Send Messages, Embed Links, Add Reactions) and paste it intoDISCORD_BOT_INVITE_URL. Optionally setGROWLIVE_DISCOVERY_CHANNEL_IDto a channel in a server you run, to enable the opt-in cross-posting Discovery feature. - Stripe: see setup steps above.
- SESSION_SECRET: any long random string.
npm startVisit BASE_URL and log in with Twitch — monitoring starts immediately
(Free tier posts into your own Twitch chat). Upgrade to Pro/Ultra and add
Discord channels from the dashboard to get clips posted there too.
- This ships with SQLite (
better-sqlite3) so you can launch with zero external infra, but it needs a persistent disk to survive restarts — on Render specifically, that means the Starter instance type or above (the Free tier has no attachable disk, so the database resets on every redeploy). PointDATABASE_FILEat the mounted disk path. - Schema changes after initial launch go through the
addColumnIfMissingmigration helper indb.js, not just editing theCREATE TABLE— that only applies to a brand-new database, it's a no-op against a users table that already exists and has real data in it. BASE_URLmust be your real public HTTPS URL in production (Twitch, Google, and Stripe all require HTTPS redirect/webhook URLs).- Twitch and YouTube access/refresh tokens are stored in plaintext in SQLite right now. Before handling real customers' tokens, encrypt them at rest — flagged here rather than done silently since it changes the schema.
- Run this as a long-lived process (systemd, Docker, a small always-on host) rather than serverless — it holds persistent EventSub WebSocket connections per active tenant, YouTube polling loops for connected Ultra users, plus one long-lived Discord connection.
Tiers stack — each one includes everything the tier below it does.
- Free: on a spike, GrowLive creates a Twitch clip and posts it into the
streamer's own Twitch chat (
src/twitch/chatSender.js, requires theuser:write:chatscope). Sensitivity locked to Medium, capped atFREE_CLIP_MONTHLY_CAPclips/month (default 15). Can still opt into Discovery cross-posting (see below) — that's available at every tier. - Pro ($2.99/mo): everything Free does, plus that same clip also posts to up to 2 of the streamer's own Discord channels (auto-post or mod-approval-gated, their choice), unlimited clips, adjustable sensitivity (Low/Medium/High presets, or fully custom velocity/floor/cooldown values).
- Ultra ($5.99/mo): everything Pro does, plus unlimited Discord
channels, and — if they link a YouTube account — simultaneous monitoring
of Twitch and YouTube chat, posting whichever platform's chat actually
spikes into that platform's native chat plus their Discord channels. A
shared cross-platform cooldown (
CROSS_PLATFORM_COOLDOWN_MSintenantManager.js) stops both platforms from firing within the same short window and double-posting everywhere.
An opt-in checkbox that cross-posts a streamer's clips to a Discord server
GrowLive itself runs (GROWLIVE_DISCOVERY_CHANNEL_ID), giving smaller
streamers extra visibility and giving GrowLive a discovery/growth channel.
Not gated by plan tier since it doesn't require the streamer to configure
anything beyond checking a box.
- YouTube integration is unverified against a real broadcast. Everything
in
src/youtube/was built against YouTube Data API v3 documentation but hasn't been exercised against an actual live stream yet — treat the first real run of this the way the Twitch EventSub scope/condition bugs needed debugging earlier in this project (check logs, expect to iterate). Likely first issues: OAuth scope/consent edge cases,liveBroadcasts.listnot returning what's expected for certain stream setups, and the exact behavior once a broadcast ends (chat 403/404 handling is a best guess). - YouTube clips are timestamp links, not real clips. YouTube has no
public API for programmatically creating a trimmed clip from a live
stream the way Twitch's Create Clip API does.
youtube.com/watch?v=ID&t=Xsis the realistic alternative — a real trimmed clip would require capturing and re-encoding the live stream yourself, real video infrastructure that doesn't exist in this codebase. - Clip retention policies beyond the free monthly cap aren't implemented.
- EventSub reconnect race: the "this close was expected" flag is set directly on the socket object being closed, not a closure variable.
- Discord embed footer:
.setFooter()is only called with non-empty text. - Spike detector baseline chasing: baseline velocity freezes once the
window already holds spike-territory activity, so an active burst can't
drag up the very threshold it's being measured against (see the comment
in
spikeDetector.js's_updateBaseline).