setup-guides

YouTube Shorts upload — setup guide

What this enables: a super-admin can click ▶ Post to YouTube on any candidate at /dashboard/admin/social-posting and the season's rendered 9:16 trailer uploads to the connected YouTube channel as a Short, with the auto-drafted caption as the video description. The resulting Shorts URL is auto-logged to social_post_log (migration 343).

This guide covers the Google Cloud Console setup + the Railway env-var configuration. Mirrors the pattern documented in GOOGLE_CALENDAR_SETUP.md — if you've set up that integration, you already understand the OAuth shape.


1. Google Cloud Console — project + API + OAuth client

You can reuse the same Google Cloud project as the Calendar integration (one project, multiple enabled APIs) or create a separate project for the YouTube brand channel. Either works; pick whichever matches your account ownership story (a brand-account project keeps graphene.fm's YouTube credentials separated from any personal-Google config).

Enable the YouTube Data API v3

  1. Open the Google Cloud Console: https://console.cloud.google.com/
  2. Select or create a project.
  3. Navigate to APIs & Services → Library.
  4. Search for "YouTube Data API v3" → click it → Enable.

If you haven't already configured the consent screen for this project:

  1. Navigate to APIs & Services → OAuth consent screen.
  2. User type: External (unless your org uses Workspace, in which case Internal works too).
  3. Fill in app name (e.g. "EmberKiln"), user support email, developer contact email.
  4. Scopes — add the three we use:
    • https://www.googleapis.com/auth/youtube.upload
    • https://www.googleapis.com/auth/youtube.readonly
    • https://www.googleapis.com/auth/userinfo.email
  5. Test users — while the app is in "Testing" status, only listed test users can complete OAuth. Add the Google account that owns the channel you'll upload to. (Production-publishing the consent screen requires Google review for the youtube.upload scope; not required for internal-tool use — you can leave the app in Testing indefinitely as long as the connected channel is on the test-users list.)

Create the OAuth 2.0 client

  1. Navigate to APIs & Services → Credentials → Create credentials → OAuth client ID.

  2. Application type: Web application.

  3. Name: e.g. "EmberKiln — YouTube Shorts".

  4. Authorized redirect URIs: add the one matching your backend deployment:

    • Production: https://hivejournalbackend-production.up.railway.app/api/youtube/oauth/callback
    • Local dev: http://localhost:3001/api/youtube/oauth/callback

    (Whatever path you put in YOUTUBE_OAUTH_REDIRECT_URI below must exactly match what's registered here.)

  5. After creation, copy the Client ID and Client secret — you need them for the env vars below.


2. Backend env vars (Railway)

Add these to the Railway production env (Settings → Variables → Add Variable):

YOUTUBE_OAUTH_CLIENT_ID=<from step 4 above>
YOUTUBE_OAUTH_CLIENT_SECRET=<from step 4 above>
YOUTUBE_OAUTH_REDIRECT_URI=https://hivejournalbackend-production.up.railway.app/api/youtube/oauth/callback

For local dev (apps/backend/.env):

YOUTUBE_OAUTH_CLIENT_ID=<same client id>
YOUTUBE_OAUTH_CLIENT_SECRET=<same client secret>
YOUTUBE_OAUTH_REDIRECT_URI=http://localhost:3001/api/youtube/oauth/callback

The frontend doesn't need any new env vars — it just calls /api/youtube/oauth/start which redirects to Google, and Google's callback redirects back to /dashboard/admin/social-posting with a ?yt=connected flash param.


3. Apply migration 345

supabase/migrations/345_youtube_credentials.sql creates the singleton table for storing the active channel's OAuth tokens. Apply via the Supabase dashboard SQL editor (file-based migrations live in the repo; the MCP-connected project is staging, not prod — see CLAUDE.md).


4. Connect the channel

  1. Navigate to /dashboard/admin/social-posting as a super-admin.
  2. Make sure platform = YouTube Shorts (default).
  3. Click ▶ Connect YouTube in the purple connection bar.
  4. Google's consent screen opens. Sign in as the channel-owning Google account (must be on the test-users list from step 1).
  5. Approve the three scopes.
  6. Google redirects back to /dashboard/admin/social-posting?yt=connected.
  7. The connection bar now shows ▶ YouTube connected with the channel title + handle + email.

5. Post your first Short

  1. On the same page, a candidate card shows the rendered 9:16 MP4 + auto-drafted caption.
  2. Click ▶ Post to YouTube on the card.
  3. The backend downloads the vertical trailer, uploads it via the YouTube Data API as multipart/related, and returns the resulting videoId + Shorts URL.
  4. On success: a sky-blue flash shows Posted to YouTube → https://www.youtube.com/shorts/<id>, the candidate moves into the recent-posts log below, and YouTube's own processing kicks off (you'll see the new Short on the channel in 30-60s once their transcoder finishes).

Quota notes

YouTube Data API has a 10,000-unit daily quota by default. Each videos.insert call costs 1,600 units → roughly 6 uploads per day on the default. The channels.list + userinfo calls during connect cost ~5 units total.

If you start pushing more than ~5 uploads/day, request a quota increase via APIs & Services → YouTube Data API v3 → Quotas → Edit quotas.

The error from quota exhaustion is:

YouTube upload failed: 403 quotaExceeded

→ Wait until the daily reset (~midnight Pacific) or request the increase.


How to verify it's working

  • Backend log: [youtube] prefixed entries from routes/youtube.ts
  • Backend table: select count(*) from youtube_credentials where is_active = true; — should return 1 when connected.
  • Test the status route directly: curl -H "Authorization: Bearer <token>" https://hivejournalbackend-production.up.railway.app/api/youtube/status

Troubleshooting

"YouTube OAuth env vars not configured" — env vars missing on Railway. Add the three vars from step 2 and re-deploy.

"redirect_uri_mismatch" — the redirect URI registered in Google Cloud Console doesn't exactly match the one in YOUTUBE_OAUTH_REDIRECT_URI. Path + scheme + host all have to match byte-for-byte. Check for trailing slashes.

"access_denied" after consent — the Google account isn't on the test-users list. Add it under OAuth consent screen → Test users.

"YouTube upload returned no videoId" — the upload succeeded at the HTTP level but YouTube rejected the video metadata. Check the backend log for the full response body. Most common cause: the description exceeds 5000 chars.

Uploaded video doesn't appear as a Short — confirm:

  • Aspect ratio ≥ 9:16 (our renderer ships 1080×1920 — pass)
  • Duration ≤ 60s (our trailers are 30-60s — pass)
  • #Shorts in title or description (the service auto-appends this to the description if missing — pass)

If all three are satisfied and it's still showing up as a regular video, YouTube's classifier sometimes takes 30-60 minutes to re-categorize after upload. Refresh the channel after a delay.

YOUTUBE UPLOAD SETUP — Docs | HiveJournal