This guide explains our Facebook Page integration in depth: goals, security posture, scopes, token flows, data we persist, and how publishing works.
Quick links: Frontend • Backend
Goals and constraints
- Multi-account support: Users can connect multiple Facebook Pages
- We never post to a personal profile; publishing is Page-only.
- We prefer Page-scoped access tokens for posting to
/PAGE_ID/feed. - We store the minimum necessary data to render UI and perform posting.
Why we do it this way
- Page access token is required to post to a Page feed. User tokens are insufficient for Page posting.
- Users explicitly choose which Page to grant to the app using Facebook’s Page selector. We capture that selection as the first granted Page from the
me/accountsresponse constrained by granted scopes. - Long‑lived tokens reduce login churn. We exchange the short‑lived token for a long‑lived user token, and we also persist the Page access token returned by
me/accounts.
References: Facebook Page Graph API, Facebook User Graph API
Permissions and scopes
Default scopes requested:
public_profileemailpages_show_listpages_read_engagementpages_manage_postspages_manage_metadatabusiness_management
Notes:
pages_show_listand/orpages_manage_metadataare used to enumerate and access granted Pages viame/accountsand to retrieve Page tokens.pages_manage_postsis required to publish to/PAGE_ID/feed.business_managementhelps in cases where the Page is owned by a Business Manager and tokens for pages would otherwise be missing.- We set
auth_type=rerequestto force the Page selector dialog so users can re‑grant if they change their choice.
Backend
See also: Frontend
Token flow
- Start: We build the Facebook OAuth URL with the scopes above and state for CSRF protection. We store
{ uid, provider: 'facebook', returnTo }in FirestoreoauthStates. - Callback: Exchange
codefor a short‑lived user token. - Page discovery:
- First try
me/accountsusing the short‑lived token to get granted Pages and their Page access tokens. - If empty, exchange for a long‑lived token, then:
- Use
debug_tokento extract granular scopes and target Page IDs, then fetch each Page directly to obtain the Page access token; or - Retry
me/accountswith the long‑lived token.
- Use
- First try
- Selection: Users can connect multiple Pages. Each Page is persisted separately.
- Persist: We write to
users/{uid}/integrations/facebook/{pageId}:accessToken(user long‑lived)pageId,pageAccessToken,accountNameprofile(for UI:platformId,displayName,avatarUrl,accountType: 'page')expiresIn,expiresAt,updatedAt
We log the entire flow using [facebook] tagged console messages to aid troubleshooting.
Multi-Account Support
Users can connect multiple Facebook Pages. Each Page is stored separately with a unique accountId.
Storage path: users/{uid}/integrations/facebook/{accountId}
Where accountId is the Facebook Page ID.
Account selection:
- If user has one Page: Automatically selected, no
accountIdrequired in API requests - If user has multiple Pages:
accountIdis required in API requests (e.g.,/v1/posts) - Missing required
accountIdreturns400error with codemissing_account
Data model
Firestore path: users/{uid}/integrations/facebook/{accountId}
Fields:
accountId: Unique identifier for this Page (same aspageId)accessToken: Long‑lived user tokenpageId: Selected Page IDpageAccessToken: Selected Page's Page token (confers Page context)accountName: Page nameprofile: UI profile cardplatformId,displayName,username,avatarUrl,accountType
expiresIn,expiresAt,updatedAtenabled_for_repost: (optional) boolean flag for automatic reposting
Publishing
Endpoint: Cloud Function publishFacebook (via orchestrator)
Flow:
- Resolve account: Determine
accountIdfrom request or auto-select if only one Page exists - Load
users/{uid}/integrations/facebook/{accountId}. - Use stored
pageIdandpageAccessToken. - POST to
/${pageId}/feedwith{ message, access_token: pageAccessToken }. - Return
{ ok: true, pageId, postId }.
Common errors:
- Missing
pageAccessToken: grant did not include the Page. Reconnect; the dialog isrerequest. - Permissions error: ensure
pages_manage_postsis approved for the app and granted by the user.
Observability
- All critical steps log with prefix
[facebook]. - Examine Cloud Function logs for: starting auth, token exchange, pages response counts/IDs, selected page, Firestore write summary, publish attempts, and failures.
Security posture
- Tokens are stored under the authenticated user document and not exposed to other users.
- We store only the minimum required Page and profile data.
- We honor Facebook rate limits and handle OAuth errors; sensitive error bodies are logged only in server logs.
Frontend
See also: Backend
Hooks and services:
apps/web/src/features/integrations/hooks/useConnectIntegration.ts- Starts OAuth by calling
startAuth('facebook', idToken, returnTo, isLocal).
- Starts OAuth by calling
apps/web/src/features/integrations/hooks/usePublishFacebook.ts- Triggers a publish by calling
publishFacebook(functionsBase, idToken).
- Triggers a publish by calling
apps/web/src/features/integrations/services/publish.ts- Implements
POST {functionsBase}/fbPublish.
- Implements
UI touchpoints:
apps/web/src/features/integrations/components/PlatformPicker.tsxrenders the connected Page usingintegrations.facebook.profile.apps/web/src/app/settings/page.tsxinitiates connect via the hook.
Request/response shapes:
- Connect: handled server‑side; client receives
{ authUrl }and redirects. - Publish: no body; server posts to
/PAGE_ID/feedand returns a textual result or JSON{ ok, pageId, postId }depending on implementation details.
Error handling:
- The publish hook throws text from the HTTP response; common cases are missing Page token or insufficient permissions.