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
| Piece | Where |
|---|---|
Provider implementations (SocialProvider interface) | app/Providers/Social/*Provider.php |
| Registry + feature flags | app/Providers/Social/SocialRegistry.php |
| Connect / callback / disconnect UI | app/Modules/Social → /dashboard/social |
| Composer, posts, calendar, publish worker | app/Modules/Publisher → /dashboard/publisher, /dashboard/posts, /dashboard/calendar |
| Live directory + polling | app/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 markedexpiredand must be reconnected.live.poll(2 min) — checks Twitch / YouTube / KICK accounts, recordslive_streams, flips the creator profile live badge and firesstream.started/stream.endedautomations.
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
| Provider | Env vars | Scopes requested | Connect | Live status | Publish | Approval / notes |
|---|---|---|---|---|---|---|
| Twitch | TWITCH_CLIENT_ID/SECRET | user:read:email | works | works (Helix streams) | n/a (no post API) | None. Follower counts need moderator:read:followers (not requested). |
| YouTube | GOOGLE_CLIENT_ID/SECRET | youtube.readonly, youtube.upload | works | liveBroadcasts?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. |
| Discord | DISCORD_CLIENT_ID/SECRET | identify, guilds | works; also sets users.discord_id for role delivery | n/a | n/a | None. |
| KICK | KICK_CLIENT_ID/SECRET | user:read, channel:read (PKCE) | works | public/v1/channels (verify field names) | n/a | Register app at kick.com/settings/developer. |
| X | X_CLIENT_ID/SECRET | tweet.read, tweet.write, users.read, offline.access (PKCE) | works | n/a | text only, POST /2/tweets | Paid/limited tier: Free tier ~1,500 posts/month/app. Media upload not implemented. |
| Facebook Pages | META_APP_ID/SECRET | pages_show_list, pages_manage_posts, pages_read_engagement | works (first managed Page) | n/a | text + single image (/feed, /photos) | Meta App Review + Business verification before non-admin users can connect. Tokens are long-lived (~60 d), no refresh → reconnect. |
META_APP_ID/SECRET | instagram_basic, instagram_content_publish, pages_show_list | works (IG professional account linked to a Page) | n/a | image only (container → media_publish) | App Review for instagram_content_publish; Business/Creator account required; image must be a public URL. | |
| TikTok | TIKTOK_CLIENT_KEY/SECRET | user.info.basic, video.publish | works | n/a | video 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. |
REDDIT_CLIENT_ID/SECRET | identity, submit | works | n/a | self/link post: line 1 r/subreddit, line 2 title, rest body | Register a "web app"; API access is rate-limited (100 req/min). | |
LINKEDIN_CLIENT_ID/SECRET | openid, profile, w_member_social | works | n/a | text 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
- Create the app on the platform's developer portal and add the redirect URI above.
- Put the client id/secret in
.env(see.env.example) — the provider card on/dashboard/socialturns from *unavailable* to *Connect*. - Connect from
/dashboard/socialas a workspace staff member or higher. The account is attached to the workspace's first creator profile. - 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.revealedin the audit log,last_revealed_aton 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://orrtmps://. Presets live inApp\Modules\Social\Services\StreamPresetsand come from each platform's own streaming-software instructions:
| Platform | Server URL preset | Backup | Where the key lives |
|---|---|---|---|
| Twitch | rtmp://live.twitch.tv/app (auto-routes to nearest ingest; regional list at help.twitch.tv/s/twitch-ingest-recommendation) | rtmps://live.twitch.tv:443/app | Creator Dashboard → Settings → Stream |
| YouTube | rtmp://a.rtmp.youtube.com/live2 (RTMPS: rtmps://a.rtmps.youtube.com:443/live2) | rtmp://b.rtmp.youtube.com/live2?backup=1 | YouTube Studio → Go live → Stream |
| KICK | rtmps://fa723fc1b171.global-contribute.live-video.net/app — confirm against the URL shown next to your key | — | Creator Dashboard → Settings → Stream key |
rtmps://live-api-s.facebook.com:443/rtmp/ | — | Live Producer → Streaming software (enable persistent key) | |
rtmps://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 |
| Trovo | rtmp://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 statuspublished/partial/failed. post.publishedautomation 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.