This guide explains our Instagram integration: scopes, token exchanges, data persistence, and media publishing.
Quick links: Frontend • Backend
Goals and constraints
- Multi-account support: Users can connect multiple Instagram accounts
- Use long‑lived tokens to minimize re‑auth.
- Support image and reel publishing via the Instagram Graph API.
Why we do it this way
- Instagram’s OAuth issues a short‑lived token which must be exchanged for a long‑lived token. We persist the long‑lived token for stability.
- The Graph API requires
igUserIdto create and publish media. We capture it at auth time from the short‑lived token exchange.
Permissions and scopes
Default scopes requested:
instagram_business_basicinstagram_business_content_publish
These enable basic account info retrieval and media publishing (images and reels).
Backend
See also: Frontend
Token flow
- Start: Build Instagram OAuth URL with
instagram_business_basic instagram_business_content_publishandstate. Persist{ uid, provider: 'instagram', returnTo }inoauthStates. - Callback: Exchange
codefor short‑lived token viahttps://api.instagram.com/oauth/access_token. - Exchange short‑lived for long‑lived token via
https://graph.instagram.com/access_tokenwithig_exchange_token. - Fetch profile for UI via
GET me?fields=id,username,account_type,media_count,profile_picture_urlusing the long‑lived token. - Persist to
users/{uid}/integrations/instagram/{accountId}(whereaccountId=igUserId):accessToken,tokenType,expiresIn,expiresAt,updatedAtprofilewithplatformId,displayName/username,avatarUrl,accountTypeextra.igUserIdfrom the short‑lived exchange
Multi-Account Support
Users can connect multiple Instagram accounts. Each account is stored separately with a unique accountId.
Storage path: users/{uid}/integrations/instagram/{accountId}
Where accountId is the Instagram user ID (igUserId).
Account selection:
- If user has one account: Automatically selected, no
accountIdrequired in API requests - If user has multiple accounts:
accountIdis required in API requests (e.g.,/v1/posts) - Missing required
accountIdreturns400error with codemissing_account
Data model
Firestore path: users/{uid}/integrations/instagram/{accountId}
Fields:
accountId: Unique identifier for this Instagram account (same asextra.igUserId)accessToken,tokenType,expiresIn,expiresAt,updatedAtprofile:platformId,displayName,username,avatarUrl,accountTypeextra.igUserId: numeric IG user id used for publishingenabled_for_repost: (optional) boolean flag for automatic reposting
Publishing
Endpoint: Cloud Function publishInstagram (via orchestrator)
Flow:
- Resolve account: Determine
accountIdfrom request or auto-select if only one account exists - Load
users/{uid}/integrations/instagram/{accountId}. - Validate
extra.igUserIdandaccessToken. - Create media container:
- Image: POST
/{igUserId}/mediawithimage_url, optionalcaption. - Video/Reel: POST with
video_urlandmedia_type=REELS. Poll status untilFINISHED.
- Image: POST
- Publish container: POST
/{igUserId}/media_publish?creation_id={container.id}. - Return
{ ok: true, id }.
Common errors:
- Missing
igUserId: reconnect Instagram. - Invalid media URL or unsupported mime type.
Observability
- Errors are surfaced in responses; detailed failures in server logs.
Security posture
- Tokens and ids are stored per user; no cross‑tenant access.
- We do not store unnecessary profile information.
Frontend
See also: Backend
Hooks and services:
apps/web/src/features/integrations/hooks/useConnectIntegration.ts- Starts OAuth by calling
startAuth('instagram', idToken, returnTo, isLocal).
- Starts OAuth by calling
apps/web/src/features/integrations/hooks/usePublishInstagram.ts- Triggers photo/reel publishing via
publishInstagram(functionsBase, idToken, imageUrl, caption?).
- Triggers photo/reel publishing via
apps/web/src/features/integrations/services/publish.ts- Implements
POST {functionsBase}/igPublishwith{ imageUrl, caption? }.
- Implements
UI touchpoints:
apps/web/src/features/integrations/components/PlatformPicker.tsxrenders the connected IG profile usingintegrations.instagram.profile.apps/web/src/app/settings/page.tsxinitiates connect via the hook.
Request/response shapes:
- Publish:
{ imageUrl: string, caption?: string }→{ ok: true, id }.
Error handling:
- The publish hook throws HTTP text on errors; ensure
imageUrlis provided.