Skip to main content

Threads

This guide explains our Threads integration in depth: goals, security posture, scopes, token exchanges, data we persist, and how publishing works.

Quick links: FrontendBackend

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_basic
  • threads_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 extract uid.
  • Persists a CSRF state in Firestore at oauthStates/{state} with { uid, provider: 'threads', returnTo }.
  • Builds the authorize URL https://www.threads.net/oauth/authorize with:
    • client_id
    • redirect_uri
    • response_type=code
    • scope=threads_basic,threads_content_publish
    • state

2) Callback: thAuthCallback

  • Validates state from oauthStates/{state}; rejects if missing.
  • Exchanges code for 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: id
    • displayName: name || username
    • username: username || name
    • avatarUrl: threads_profile_picture_url
    • accountType: 'user'

3) Persist

Firestore path: users/{uid}/integrations/threads/{platformId} (merge):

  • accountId: Unique identifier (same as platformId)
  • accessToken, tokenType, expiresIn, expiresAt, updatedAt
  • profile: platformId, displayName, username, avatarUrl, accountType
  • extra.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 accountId required in API requests
  • If user has multiple accounts: accountId is required in API requests (e.g., /v1/posts)
  • Missing required accountId returns 400 error with code missing_account

Publishing

Endpoint: Cloud Function publishThreads (via orchestrator)

Flow:

  1. Resolve account: Determine accountId from request or auto-select if only one account exists
  2. Load users/{uid}/integrations/threads/{accountId} and validate accessToken.
  3. 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)
  4. 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>
    • Publish: POST https://graph.threads.net/v1.0/me/threads_publish with form creation_id=<container.id> and same Bearer header
  5. 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 include fbtrace_id for 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 extra as needed.

Gotchas and troubleshooting

  • Use the .net authorize domain (https://www.threads.net/oauth/authorize), not .com.
  • Scopes should be comma‑separated.
  • redirect_uri must 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.

Frontend

See also: Backend

Hooks and services:

  • apps/web/src/features/integrations/hooks/useConnectIntegration.tsstartAuth('threads', ...).
  • apps/web/src/features/integrations/hooks/usePublishThreads.tspublishThreads(functionsBase, idToken, text?, imageUrl?).
  • apps/web/src/features/integrations/services/publish.tsPOST {functionsBase}/thPublish.

UI touchpoints:

  • apps/web/src/features/integrations/components/PlatformPicker.tsx renders integrations.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