Skip to content

Latest commit

 

History

42 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GrowLive

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.

Why Stripe, and how billing is wired

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:

  1. 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) into STRIPE_PRO_PRICE_ID and STRIPE_ULTRA_PRICE_ID.
  2. API keys. Developers → API keys. Use Test mode keys while developing, switch to live when ready for real payments.
  3. 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_...) into STRIPE_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.

Architecture

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

Dashboard is multiple pages, not one long form

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.

Setup

npm install
cp .env.example .env

Fill 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 (scope bot, permissions: Send Messages, Embed Links, Add Reactions) and paste it into DISCORD_BOT_INVITE_URL. Optionally set GROWLIVE_DISCOVERY_CHANNEL_ID to 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 start

Visit 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.

Deploying for real

  • 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). Point DATABASE_FILE at the mounted disk path.
  • Schema changes after initial launch go through the addColumnIfMissing migration helper in db.js, not just editing the CREATE 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_URL must 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.

Free vs Pro vs Ultra (enforced server-side)

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 the user:write:chat scope). Sensitivity locked to Medium, capped at FREE_CLIP_MONTHLY_CAP clips/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_MS in tenantManager.js) stops both platforms from firing within the same short window and double-posting everywhere.

GrowLive Discovery (any tier)

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.

What's not fully built / needs live testing

  • 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.list not 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=Xs is 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.

Known fixes carried over from the self-hosted version

  • 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).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages