Discourse plugin for the ChrisTitusTech community forum. Bundles:
- GitHub Sponsors — syncs sponsoring users to a Discourse group via GitHub OAuth + GraphQL
- Twitch Subscribers — syncs subscribers via Twitch OAuth + Helix API
- YouTube Members — syncs channel members via Google OAuth + YouTube Data API v3
- Chris Titus Tech dark theme — mirrors the design of christitus.com (PT Sans,
#47c4f1accent, dark#212529background)
Discourse must already be running. Add the plugin by editing your containers/app.yml:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone https://github.com/discourse/docker_manager.git
- git clone https://github.com/ChrisTitusTech/community.git discourse-community-integrationsThe clone target name
discourse-community-integrationsis required. Font asset paths in the theme CSS use this directory name.
Then rebuild:
cd /var/discourse
./launcher rebuild appAfter the rebuild, activate the theme in Admin → Customize → Colors → Chris Titus Tech → Set as Default.
plugin.rb ← Discourse plugin entrypoint
app/lib/
│ auth/
│ │ twitch_strategy.rb ← OmniAuth OAuth2 strategy for Twitch
│ │ twitch_authenticator.rb ← Discourse authenticator wrapper
│ │ youtube_authenticator.rb ← Google OAuth connect-only (YouTube scope)
│ └── community_integrations/
│ group_sync.rb ← Shared add/remove helper
│ twitch_checker.rb ← Helix subscriber API
│ github_sponsors_checker.rb ← GitHub GraphQL sponsorship check
│ youtube_member_checker.rb ← YouTube memberships API (two-token)
config/
│ settings.yml ← Plugin site settings
│ locales/server.en.yml ← Admin UI label strings
jobs/
│ regular/ ← Async jobs triggered on login
│ └── scheduled/ ← Periodic full re-sync (every 6 h)
assets/
│ fonts/ ← PT Sans woff2 (reference; font loaded via Google Fonts)
│ stylesheets/
│ common/ctt-theme.scss ← Dark theme — full color + typography
│ desktop/ctt-desktop.scss ← Desktop-specific overrides
│ └── mobile/ctt-mobile.scss ← Mobile-specific overrides
deploy/
│ app.yml ← Full Discourse Docker config (reference)
scripts/
setup.sh ← One-time Ubuntu 22.04 VPS prep
get_youtube_creator_token.py ← Get the YouTube creator refresh token
All settings are under Admin → Settings (search community_integrations) or via Admin → Plugins → discourse-community-integrations → Settings.
| Setting | Description |
|---|---|
community_integrations_enabled |
Master switch |
community_integrations_twitch_client_id / community_integrations_twitch_client_secret |
Twitch OAuth app credentials |
community_integrations_twitch_broadcaster_id |
Numeric channel ID (not username) |
community_integrations_twitch_subscriber_group |
Discourse group name for subscribers (default: Twitch Subscriber) |
community_integrations_github_sponsors_target_username |
GitHub username to check sponsorship of (default: ChrisTitusTech) |
community_integrations_github_sponsors_group |
Discourse group for sponsors (default: GitHub Sponsors) |
community_integrations_youtube_client_id / community_integrations_youtube_client_secret |
Google OAuth app credentials |
community_integrations_youtube_channel_id |
Creator's YouTube channel ID |
community_integrations_youtube_creator_refresh_token |
Creator token with channel-memberships.creator scope |
community_integrations_youtube_member_group |
Discourse group for members (default: YouTube Member) |
community_integrations_sync_interval_hours |
Re-sync interval in hours (1–24, default: 6) |
Error 400 /
invalid_request: Redirect URI http://… is not supported— Discourse is generatinghttp://callback URLs instead ofhttps://. The fix is to addDISCOURSE_FORCE_HTTPS: trueto yourdeploy/app.yml(it is already present in thedeploy/app.ymlin this repo). Without it, Discourse constructs callback URLs withhttp://even when the site is served over TLS, and every OAuth provider will reject them.After adding the setting, run
./launcher rebuild appto apply it.Other causes of redirect URI errors: wrong path (copy character-for-character from the table below), mismatched domain in
DISCOURSE_HOSTNAME, or a trailing slash where none is expected.
| Provider | Callback URL to enter in developer console |
|---|---|
| GitHub | https://YOUR_DOMAIN/auth/github/callback |
| Google (login) | https://YOUR_DOMAIN/auth/google_oauth2/callback |
| YouTube (connect; our plugin) | https://YOUR_DOMAIN/auth/youtube/callback |
| Twitch (our plugin) | https://YOUR_DOMAIN/auth/twitch/callback |
| Patreon | https://YOUR_DOMAIN/auth/patreon/callback |
Common mistakes:
- Google path is
/auth/google_oauth2/callback— the_oauth2suffix and trailing/callbackare both required - Patreon path is
/auth/patreon/callback— not/auth/patreon-oauth2/callback - All URLs must use
https://— Discourse rejectshttpOAuth flows - GitHub only accepts one callback URL per app; it must match exactly (no trailing slash)
- https://github.com/settings/developers → OAuth Apps → New OAuth App
- Authorization callback URL:
https://YOUR_DOMAIN/auth/github/callback - Enter Client ID + Secret in Admin → Settings → Login → GitHub and enable
enable github logins
GitHub OAuth is standard Discourse — no custom settings needed. The plugin checks sponsorship automatically on each GitHub login.
- https://www.patreon.com/portal/registration/register-clients → Create Client
- Redirect URIs:
https://YOUR_DOMAIN/auth/patreon/callback - Enable the Patreon plugin in Admin → Plugins → Patreon and enter Client ID + Secret
- Log in to the forum as the creator's Patreon account once — the plugin captures the creator token on that login
- In Admin → Plugins → Patreon, map your reward tiers to the
PatronDiscourse group
- https://dev.twitch.tv/console → Register Your Application
- OAuth Redirect URL:
https://YOUR_DOMAIN/auth/twitch/callback - Category: Website Integration
- Enter Client ID, Secret, and your numeric Broadcaster ID in Admin → Settings (search
community_integrations_twitch)
Find your numeric broadcaster ID (it is not your username):
- API:
https://api.twitch.tv/helix/users?login=YOUR_USERNAME(requires a bearer token) - Or use: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
YouTube membership verification uses two tokens and one Google OAuth app for both standard Google login and the YouTube connect flow.
| Token | Who | Scope | Purpose |
|---|---|---|---|
| Creator token | Channel owner | youtube.channel-memberships.creator |
Query the member list |
| User token | Each forum member | youtube.readonly |
Resolve their channel ID |
Step 1 — Create a Google OAuth app
- https://console.cloud.google.com → new project → enable YouTube Data API v3
- APIs & Services → Credentials → Create OAuth Client ID → Web Application
- Authorized JavaScript origins (required by Google — base domain only, no path):
https://YOUR_DOMAIN
- Authorized redirect URIs (both required):
https://YOUR_DOMAIN/auth/google_oauth2/callbackhttps://YOUR_DOMAIN/auth/youtube/callback
- Copy the Client ID and Client Secret
Step 2 — Get the creator refresh token (one-time)
pip install google-auth-oauthlib
python3 scripts/get_youtube_creator_token.pyPaste the printed refresh token into plugin settings.
Step 3 — Configure in Discourse
- Admin → Settings → Login → Google — enter Client ID and Secret; enable
enable google oauth2 logins - Admin → Settings (search
community_integrations_youtube) — enter YouTube Client ID, Secret, Channel ID, Creator Refresh Token
Create these groups in Admin → Groups before enabling integrations (names must match plugin settings exactly, or update the settings to match):
| Group | Platform |
|---|---|
GitHub Sponsors |
GitHub |
Twitch Subscriber |
Twitch |
YouTube Member |
YouTube |
Run the all-in-one debug script from inside the container:
# Copy script into the container's shared volume, then run it
cp scripts/debug_plugin.sh /var/discourse/shared/standalone/debug_plugin.sh
cd /var/discourse && ./launcher enter app
bash /shared/debug_plugin.shThe script checks:
- All plugin files are present on disk
- Plugin appears in the Discourse registry
- All
community_integrations_*settings are non-empty - Required groups exist (
Twitch Subscriber,GitHub Sponsors,YouTube Member) - Auth providers (Twitch, YouTube) are registered and enabled
- Redis is reachable
- Sidekiq job classes are loaded
- Sidekiq dead/retry queue for plugin job failures
- Recent errors in
production.logfor this plugin
# Quick one-liners
# Tail logs for sync activity
sudo /var/discourse/launcher logs app | grep -E "(TwitchChecker|GithubSponsor|YoutubeMember|ERROR)"
# Manually trigger a full re-sync
sudo /var/discourse/launcher enter app
rails r "Jobs::SyncCommunityIntegrations.new.execute({})"
# Check a user's connected accounts (replace 1 with user ID)
rails r "puts UserAssociatedAccount.where(user_id: 1).pluck(:provider_name, :provider_uid)"cd /var/discourse
./launcher rebuild appThis pulls the latest plugin code and rebuilds the container.
If you're setting up the VPS from scratch:
bash <(curl -fsSL https://raw.githubusercontent.com/ChrisTitusTech/community/main/scripts/setup.sh)This installs Docker, configures UFW (ports 22/80/443), adds a 2 GB swapfile, and clones discourse_docker. After it completes, copy deploy/app.yml to /var/discourse/containers/app.yml, fill in the REPLACE_WITH_* placeholders, then bootstrap.