YouTube
This guide documents our YouTube integration: the goals, the OAuth flow we implement in Cloud Functions, which scopes we request, how we fetch and persist channel information, and how we keep tokens fresh. It’s structured for audits, troubleshooting, and future feature work (uploads).
Quick links: Frontend • Backend
Goals and constraints
- Multi-account support: Users can connect multiple YouTube Channels
- Persist only what we need for UI and future uploads: access/refresh tokens, minimal channel profile, and basic metadata.
- Enforce required scopes server‑side so channel info is always retrievable and uploads are permitted.
Why we do it this way
- YouTube Data API returns channel details for the authenticated user only when the token has a YouTube read scope. We enforce the scopes at the server to avoid partial grants.
- We store a compact
profilefor UI (platformId,displayName,username,avatarUrl,accountType: 'channel') and keep the raw channel object underextra.channelfor observability without widening our primary schema. - Long‑lived stability: we store the
refreshTokenand provide both an on‑demand refresh endpoint and a scheduled sweep to rotate access tokens before expiry.
Backend
See also: Frontend
Permissions and scopes
Server‑enforced default scope set (merged with any incoming scopes):
https://www.googleapis.com/auth/youtube(broad read/write access to YouTube account)https://www.googleapis.com/auth/youtube.readonly(read channel info)https://www.googleapis.com/auth/youtube.upload(upload videos)https://www.googleapis.com/auth/youtube.force-ssl(some endpoints prefer this variant)
Notes:
- We accept optional client‑provided scopes, but we always merge and de‑duplicate with the required set above so essential fields are available.
- The OAuth request uses
access_type=offline,include_granted_scopes=true, andprompt=consent select_accountto force a fresh consent when needed and obtain a refresh token.
Token and profile flow
1) Start: ytAuthStart
- Requires a Firebase ID token in
Authorization: Bearer <idToken>; we verify it and extractuid. - Creates a
statedocument atoauthStates/{state}with{ uid, provider: 'youtube', returnTo }. - Builds the authorize URL
https://accounts.google.com/o/oauth2/v2/authwith:client_id,redirect_uri,response_type=codescope(merged set described above)access_type=offline,include_granted_scopes=true,prompt=consent select_account
2) Callback: ytAuthCallback
- Validates
stateby readingoauthStates/{state}; rejects if missing. - Exchanges
codefor tokens viaPOST https://oauth2.googleapis.com/tokenwithclient_id,client_secret, and exactredirect_uri. - Computes
expiresInandexpiresAtfrom the token response. - Fetches channel profile for UI via YouTube Data API:
GET https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true- Authorization:
Bearer <access_token>
- Maps the first channel to a compact display profile:
platformId: channeliddisplayName:snippet.titleusername:snippet.customUrl(if present)avatarUrl: fromsnippet.thumbnails(prefersdefaultorhigh)accountType:'channel'
- Persists to Firestore at
users/{uid}/integrations/youtube(merge):accessToken,refreshToken,tokenType,scope,expiresIn,expiresAt,updatedAtprofile(display profile for UI)channelId(same asprofile.platformId) andaccountName(same asprofile.displayName)extra.channel(raw channel object)
- Deletes the
oauthStates/{state}document. - Redirects back to
returnTowith?youtube=connectedif provided, or returns a plain success page.
3) Refreshing access tokens
ytRefreshNow(HTTP): Verifies the caller’s Firebase ID token, then refreshes this user’s YouTube tokens by calling the internal helperrefreshYouTubeForUid(uid).ytRefreshSweep(scheduled every 12h):- Scans
users/*/integrations/youtube. - If
expiresAtis within 6 hours (or missing) and arefreshTokenexists, calls the refresh helper. - Writes new
accessToken, optionally newrefreshToken, carries forwardscope, and updatesexpiresIn/At.
- Scans
Multi-Account Support
Users can connect multiple YouTube Channels. Each channel is stored separately with a unique accountId.
Storage path: users/{uid}/integrations/youtube/{accountId}
Where accountId is the YouTube Channel ID.
Account selection:
- If user has one channel: Automatically selected, no
accountIdrequired in API requests - If user has multiple channels:
accountIdis required in API requests (e.g.,/v1/posts) - Missing required
accountIdreturns400error with codemissing_account
Data model
Firestore path: users/{uid}/integrations/youtube/{accountId}
Fields we persist:
accountId: Unique identifier for this YouTube channel (same aschannelId)- Tokens and expiry:
accessToken,refreshToken,tokenType,scope,expiresIn,expiresAt,updatedAt - UI profile card (
profile):platformId,displayName,username,avatarUrl,accountType: 'channel'
- Convenience:
channelId,accountName - Debug payload (
extra.channel): raw object fromchannels.list enabled_for_repost: (optional) boolean flag for automatic reposting
We intentionally avoid storing additional private details beyond what we need for UI and uploads.
Uploads (prerequisites and plan)
We already request youtube.upload to enable future uploads. When implemented, the flow will use videos.insert with the stored access token and will follow a resumable upload process for large videos. Until then, the integration focuses on connection and channel identity for UI.
References:
- Videos insert:
https://developers.google.com/youtube/v3/docs/videos/insert
Observability
- Critical failures return HTTP errors to the client; additional details are logged on the server (only minimal, non‑sensitive snippets on errors).
- On
channels.listfailure we capture status and a truncated body to aid scope/permission troubleshooting.
Security posture
- We require a Firebase ID token for all user‑initiated endpoints and store tokens under the user’s document.
- We only persist the minimum necessary profile fields for UI; full provider payloads go under
extra.channelfor debugging. - We follow our engineering guardrails on secret handling and server‑side token exchange.
Gotchas and troubleshooting
- A 403 from
channels.listwithACCESS_TOKEN_SCOPE_INSUFFICIENTmeans the access token lacksyoutubeoryoutube.readonly. Revoke the grant in Google Account settings and reconnect. - Ensure the OAuth consent screen lists the YouTube scopes and that the YouTube Data API v3 is enabled for the same project as the OAuth client.
- If profile fields are missing, verify the channel has a
customUrland that thumbnails are present; the code safely falls back to available sizes.
Related code
Frontend
See also: Backend
Hooks and services:
apps/web/src/features/integrations/hooks/useConnectIntegration.ts→startAuth('youtube', ...).apps/web/src/features/integrations/components/PlatformPicker.tsxrendersintegrations.youtube.profile.
Note: Upload publishing hook is not yet implemented.
apps/functions/youtube.js:ytAuthStart,ytAuthCallback,ytRefreshNow,ytRefreshSweepapps/web/src/features/integrations/components/PlatformPicker.tsx: renders the integration card usingintegrations.youtube.profile
External references
- YouTube Data API:
https://developers.google.com/youtube/v3 - Channels list:
https://developers.google.com/youtube/v3/docs/channels/list