Threads
This guide explains our Threads integration in depth: goals, security posture, scopes, token exchanges, data we persist, and how publishing works.
Quick links: Frontend • Backend
Goals and constraints
- Multi-account support: Users can connect multiple Threads accounts
- Use long‑lived tokens to minimize re‑auth.
- Store only the minimum data needed to render UI and publish.
Why we do it this way
- Threads Graph issues a short‑lived access token in the OAuth callback; we immediately exchange it for a long‑lived token for stability.
- We retrieve minimal profile fields for UI only and keep the long‑lived token for future publishing.
- All sensitive exchanges happen server‑side in Cloud Functions; the browser never sees client secrets.
Backend
See also: Frontend
Permissions and scopes
Default scopes requested:
threads_basicthreads_content_publish
Notes:
- Scopes are sent as a comma‑separated list. We default to the two above and can merge additional scopes if needed in the future.
Token and profile flow
1) Start: thAuthStart
- Requires a Firebase ID token in
Authorization: Bearer <idToken>; we verify it and extractuid. - Persists a CSRF
statein Firestore atoauthStates/{state}with{ uid, provider: 'threads', returnTo }. - Builds the authorize URL
https://www.threads.net/oauth/authorizewith:client_idredirect_uriresponse_type=codescope=threads_basic,threads_content_publishstate
2) Callback: thAuthCallback
- Validates
statefromoauthStates/{state}; rejects if missing. - Exchanges
codefor a short‑lived token via:POST https://graph.threads.net/oauth/access_token- Form:
client_id,client_secret,redirect_uri,grant_type=authorization_code,code.
- Exchanges short‑lived for long‑lived token via:
GET https://graph.threads.net/access_token?grant_type=th_exchange_token&client_secret=...&access_token=...
- Fetches minimal user profile for UI via:
GET https://graph.threads.net/v1.0/me?fields=id,username,name,threads_profile_picture_url&access_token=...
- Maps to a compact profile card:
platformId:iddisplayName:name || usernameusername:username || nameavatarUrl:threads_profile_picture_urlaccountType:'user'
3) Persist
Firestore path: users/{uid}/integrations/threads/{platformId} (merge):
accountId: Unique identifier (same asplatformId)accessToken,tokenType,expiresIn,expiresAt,updatedAtprofile:platformId,displayName,username,avatarUrl,accountTypeextra.threadsUserId: numeric Threads user id from the short‑lived exchange (if provided)enabled_for_repost: (optional) boolean flag for automatic reposting
Multi-Account Support
Users can connect multiple Threads accounts. Each account is stored separately with a unique accountId.
Storage path: users/{uid}/integrations/threads/{accountId}
Where accountId is the Threads user ID (platformId).
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
Publishing
Endpoint: Cloud Function publishThreads (via orchestrator)
Flow:
- Resolve account: Determine
accountIdfrom request or auto-select if only one account exists - Load
users/{uid}/integrations/threads/{accountId}and validateaccessToken. - Text‑only (single‑step):
POST https://graph.threads.net/v1.0/me/threads- Headers:
Authorization: Bearer <access_token>,Content-Type: application/x-www-form-urlencoded - Body:
media_type=TEXT&text=...&auto_publish_text=true - Response JSON includes
{ id }(published media id)
- Image/Video (two‑step):
- Create container:
POST https://graph.threads.net/v1.0/me/threads- For image:
media_type=IMAGE&image_url=<url>&text=<optional> - For video:
media_type=VIDEO&video_url=<url>&text=<optional> - Headers:
Authorization: Bearer <access_token>
- For image:
- Publish:
POST https://graph.threads.net/v1.0/me/threads_publishwith formcreation_id=<container.id>and same Bearer header
- Create container:
- Return
{ ok: true, id }.
Common errors:
- Missing
accessToken: reconnect Threads. - Permissions or app review: ensure scopes are approved and granted by the user.
- Transient Graph errors: we map
is_transient/5xx to HTTP 503 so Cloud Tasks retries; logs includefbtrace_idfor correlation.
Observability
- Critical steps return HTTP errors; additional details are logged on the server (with sensitive bodies truncated).
Security posture
- We require a Firebase ID token for all user‑initiated endpoints and store tokens under the user’s document.
- We store only the minimum profile fields for UI. Full provider payloads are not persisted, aside from selected fields in
extraas needed.
Gotchas and troubleshooting
- Use the
.netauthorize domain (https://www.threads.net/oauth/authorize), not.com. - Scopes should be comma‑separated.
redirect_urimust exactly match your configured callback URL in the Threads app settings.- Ensure your app is in the correct mode and the connecting user is added as a tester/developer if the app is not public.
Related code
Frontend
See also: Backend
Hooks and services:
apps/web/src/features/integrations/hooks/useConnectIntegration.ts→startAuth('threads', ...).apps/web/src/features/integrations/hooks/usePublishThreads.ts→publishThreads(functionsBase, idToken, text?, imageUrl?).apps/web/src/features/integrations/services/publish.ts→POST {functionsBase}/thPublish.
UI touchpoints:
apps/web/src/features/integrations/components/PlatformPicker.tsxrendersintegrations.threads.profile.
Request/response shapes:
-
Publish body may include
{ text?: string, imageUrl?: string }; server returns{ ok: true, id }. -
apps/functions/threads.js:thAuthStart,thAuthCallback,thPublish -
apps/web/src/features/integrations/services/authStart.ts: builds start‑auth requests -
apps/web/src/features/integrations/hooks/useConnectIntegration.ts: triggers the connect flow from the UI