Social connections, publishing & live status

CreatorOS connects creator accounts through OAuth, stores tokens encrypted at rest (AES-256-GCM, ENCRYPTION_KEY), and only requests the scopes each feature uses. A provider with blank credentials in .env is simply unavailable in the UI — nothing crashes.

How it fits together

PieceWhere
Provider implementations (SocialProvider interface)app/Providers/Social/*Provider.php
Registry + feature flagsapp/Providers/Social/SocialRegistry.php
Connect / callback / disconnect UIapp/Modules/Social/dashboard/social
Composer, posts, calendar, publish workerapp/Modules/Publisher/dashboard/publisher, /dashboard/posts, /dashboard/calendar
Live directory + pollingapp/Modules/Live/live, /live/now.json, /dashboard/live

Redirect URI for every provider: https://krunc.com/social/callback/{provider} (provider keys: twitch, youtube, discord, kick, x, facebook, instagram, tiktok, reddit, linkedin).

Background work runs from php bin/worker (cron every minute with --once, or as a daemon):

  • publisher.due (60 s) — enqueues scheduled posts whose time has come (the publish job is also queued at schedule time; the scheduler is a safety net).
  • social.refresh_tokens (30 min) — refreshes tokens that expire within 30 minutes; accounts whose refresh fails are marked expired and must be reconnected.
  • live.poll (2 min) — checks Twitch / YouTube / KICK accounts, records live_streams, flips the creator profile live badge and fires stream.started / stream.ended automations.

Feature flags

Insert a row in feature_flags to switch a provider off without removing credentials (e.g. while waiting for app review):

INSERT INTO feature_flags (`key`, enabled, description, updated_at) VALUES ('social.instagram', 0, 'Waiting for Meta app review', UTC_TIMESTAMP());
-- later
UPDATE feature_flags SET enabled = 1 WHERE `key` = 'social.instagram';

A missing row means "enabled if credentials exist".

Per-platform status

ProviderEnv varsScopes requestedConnectLive statusPublishApproval / notes
TwitchTWITCH_CLIENT_ID/SECRETuser:read:emailworksworks (Helix streams)n/a (no post API)None. Follower counts need moderator:read:followers (not requested).
YouTubeGOOGLE_CLIENT_ID/SECRETyoutube.readonly, youtube.uploadworksliveBroadcasts?broadcastStatus=active (verify on a real channel)video only, resumable videos.insert (verify)Google OAuth consent-screen verification required for youtube.upload outside test users. 10k quota units/day.
DiscordDISCORD_CLIENT_ID/SECRETidentify, guildsworks; also sets users.discord_id for role deliveryn/an/aNone.
KICKKICK_CLIENT_ID/SECRETuser:read, channel:read (PKCE)workspublic/v1/channels (verify field names)n/aRegister app at kick.com/settings/developer.
XX_CLIENT_ID/SECRETtweet.read, tweet.write, users.read, offline.access (PKCE)worksn/atext only, POST /2/tweetsPaid/limited tier: Free tier ~1,500 posts/month/app. Media upload not implemented.
Facebook PagesMETA_APP_ID/SECRETpages_show_list, pages_manage_posts, pages_read_engagementworks (first managed Page)n/atext + single image (/feed, /photos)Meta App Review + Business verification before non-admin users can connect. Tokens are long-lived (~60 d), no refresh → reconnect.
InstagramMETA_APP_ID/SECRETinstagram_basic, instagram_content_publish, pages_show_listworks (IG professional account linked to a Page)n/aimage only (container → media_publish)App Review for instagram_content_publish; Business/Creator account required; image must be a public URL.
TikTokTIKTOK_CLIENT_KEY/SECRETuser.info.basic, video.publishworksn/avideo only via Content Posting API PULL_FROM_URL (verify)App audit required; unaudited apps post as SELF_ONLY; video URL domain must be verified in the TikTok developer portal.
RedditREDDIT_CLIENT_ID/SECRETidentity, submitworksn/aself/link post: line 1 r/subreddit, line 2 title, rest bodyRegister a "web app"; API access is rate-limited (100 req/min).
LinkedInLINKEDIN_CLIENT_ID/SECRETopenid, profile, w_member_socialworksn/atext only, POST /rest/posts (version 202405)"Share on LinkedIn" product must be added to the app; refresh tokens only for approved partners (60-day expiry otherwise). Organization posting needs Marketing Developer Platform.

"verify" = implemented from the official documentation but not yet exercised against a live account from this host. The provider surfaces the platform's error message verbatim if a call fails — nothing is faked as success.

Setting up each OAuth app

  1. Create the app on the platform's developer portal and add the redirect URI above.
  2. Put the client id/secret in .env (see .env.example) — the provider card on /dashboard/social turns from *unavailable* to *Connect*.
  3. Connect from /dashboard/social as a workspace staff member or higher. The account is attached to the workspace's first creator profile.
  4. For platforms that need review, keep the feature flag off until approval; you can still connect as an app tester.

Stream destinations

/dashboard/social/destinations (admin and above) stores the RTMP/RTMPS server URL + stream key for every place a creator broadcasts, so they can be copied into OBS / Streamlabs (Settings → Stream → Service *Custom…*). It is a secure address book, not a relay: video still goes from the encoder straight to each platform. Multistreaming to several destinations at once needs a multi-output plugin or a relay service.

  • Stream keys are encrypted at rest (AES-256-GCM, ENCRYPTION_KEY); lists show only the last four characters. Reveal and Copy key fetch the key over an audited JSON POST (stream_destination.revealed in the audit log, last_revealed_at on the row) — the raw key is never rendered into page HTML.
  • Deleting a destination is a hard delete so the encrypted key does not linger. Rotate a leaked key on the platform first, then update it here.
  • Server URLs must be rtmp:// or rtmps://. Presets live in App\Modules\Social\Services\StreamPresets and come from each platform's own streaming-software instructions:
PlatformServer URL presetBackupWhere the key lives
Twitchrtmp://live.twitch.tv/app (auto-routes to nearest ingest; regional list at help.twitch.tv/s/twitch-ingest-recommendation)rtmps://live.twitch.tv:443/appCreator Dashboard → Settings → Stream
YouTubertmp://a.rtmp.youtube.com/live2 (RTMPS: rtmps://a.rtmps.youtube.com:443/live2)rtmp://b.rtmp.youtube.com/live2?backup=1YouTube Studio → Go live → Stream
KICKrtmps://fa723fc1b171.global-contribute.live-video.net/appconfirm against the URL shown next to your keyCreator Dashboard → Settings → Stream key
Facebookrtmps://live-api-s.facebook.com:443/rtmp/Live Producer → Streaming software (enable persistent key)
Instagramrtmps://live-upload.instagram.com:443/rtmp/Instagram desktop → Live → Streaming software (per-broadcast key)
X*blank — per-source URL from Media Studio → Producer*Media Studio → Producer → Sources
TikTok*blank — shown under Go LIVE → Streaming software once the account has LIVE Studio access*same screen
Trovortmp://livepush.trovo.live/live/Stream Settings
Custom RTMP*blank*your server

Plan limit: limits.stream_destinations on the tenant (default 12).

Publishing rules

  • One post, many targets. Each target can carry its own text variant; the strictest character limit among selected accounts is shown live in the composer.
  • Hard limits enforced server-side: X 280, Instagram 2200, TikTok 2200, LinkedIn 3000, YouTube 5000, Facebook 63,206, Reddit title 300.
  • Media are public URLs (upload from the composer stores an image under /uploads/...). Instagram/TikTok/YouTube need media; X/LinkedIn are text-only for now.
  • Scheduled times are entered in the user's timezone (users.timezone) and stored in UTC.
  • Results are per platform: published, failed (with the platform's error, retry button), post status published / partial / failed.
  • post.published automation trigger fires when at least one target succeeds.

Live status

/live lists profiles with is_live = 1; category chips come from config('app.live_categories'). Platform categories are mapped (e.g. "Grand Theft Auto V" → "GTA RP"). A manual override from the profile editor (live_platform = manual) is never cleared by the poller.

Future work: Twitch EventSub (stream.online / stream.offline webhooks) would replace polling for Twitch and cut latency to seconds; it needs a public HTTPS callback with signature verification (Twitch-Eventsub-Message-Signature) and app access tokens. YouTube offers PubSubHubbub for uploads but not live state.