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
- Open the Google Cloud Console: https://console.cloud.google.com/
- Select or create a project.
- Navigate to APIs & Services → Library.
- Search for "YouTube Data API v3" → click it → Enable.
Configure the OAuth consent screen
If you haven't already configured the consent screen for this project:
- Navigate to APIs & Services → OAuth consent screen.
- User type: External (unless your org uses Workspace, in which case Internal works too).
- Fill in app name (e.g. "EmberKiln"), user support email, developer contact email.
- Scopes — add the three we use:
https://www.googleapis.com/auth/youtube.uploadhttps://www.googleapis.com/auth/youtube.readonlyhttps://www.googleapis.com/auth/userinfo.email
- 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
-
Navigate to APIs & Services → Credentials → Create credentials → OAuth client ID.
-
Application type: Web application.
-
Name: e.g. "EmberKiln — YouTube Shorts".
-
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_URIbelow must exactly match what's registered here.) - Production:
-
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
- Navigate to /dashboard/admin/social-posting as a super-admin.
- Make sure platform = YouTube Shorts (default).
- Click ▶ Connect YouTube in the purple connection bar.
- Google's consent screen opens. Sign in as the channel-owning Google account (must be on the test-users list from step 1).
- Approve the three scopes.
- Google redirects back to
/dashboard/admin/social-posting?yt=connected. - The connection bar now shows ▶ YouTube connected with the channel title + handle + email.
5. Post your first Short
- On the same page, a candidate card shows the rendered 9:16 MP4 + auto-drafted caption.
- Click ▶ Post to YouTube on the card.
- The backend downloads the vertical trailer, uploads it via the
YouTube Data API as
multipart/related, and returns the resulting videoId + Shorts URL. - 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 fromroutes/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)
#Shortsin 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.