Skip to content

Instagram connection via Meta Graph API OAuth (no username/password)#11

Merged
Abby263 merged 1 commit into
mainfrom
feat/instagram-oauth
May 8, 2026
Merged

Instagram connection via Meta Graph API OAuth (no username/password)#11
Abby263 merged 1 commit into
mainfrom
feat/instagram-oauth

Conversation

@Abby263
Copy link
Copy Markdown
Owner

@Abby263 Abby263 commented May 8, 2026

Summary

Pivots the Instagram connection model to Facebook Login OAuth. Users click Connect with Facebook on the Connections page, grant publishing + insights scopes, and we resolve the IG Business Account via their Page picker. Username/password storage is gone — both from the schema and the UI.

Posting now uses the Meta Graph API two-step protocol (/media/media_publish). Because the Graph API requires a public image URL (no base64, no auth), every generated image is re-uploaded to Supabase Storage before publish — both in the auto-publish LangGraph agent path and in the approval route on Vercel.

A weekly refresh-tokens cron pre-emptively exchanges long-lived tokens before their 60-day expiry; expired ones get connection_status='expired' and the UI surfaces a Reconnect button.

Breaking change

migrations/004_instagram_oauth.sql deletes existing username/password Instagram connections. Workflows that referenced them have their connection_id set to NULL (existing FK) and need to be re-pointed at a new OAuth connection. Acknowledged tradeoff per the design discussion: cleanest schema, harshest UX.

What's where

New env required

META_APP_ID
META_APP_SECRET
TOKEN_ENCRYPTION_KEY
SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY
SUPABASE_STORAGE_BUCKET           (default 'post-media')

OAuth redirect URI in your Meta App must be ${NEXT_PUBLIC_APP_URL}/api/auth/instagram/callback. The post-media Supabase Storage bucket needs Public access ON so Meta's servers can fetch image URLs.

The same env vars (minus the Meta ones) need to be set on the LangGraph runtime so the agent can call uploadImageToSupabase from inside a run.

Test plan

  • yarn lint, yarn typecheck, yarn test, yarn lint:langgraph-json, yarn format:check — all clean
  • pnpm lint (frontend) — no new errors, only pre-existing warnings
  • pnpm test:ci (frontend) — 22 pass
  • pnpm build (frontend) — all new routes compile (/api/auth/instagram/{start,callback,select-page}, /api/cron/refresh-tokens)
  • Pre-merge manual checks:
    • Run migration 004 against Supabase
    • Create the post-media bucket in Supabase Storage (Public ON)
    • Set env vars on Vercel + LangGraph
    • Register Meta App, add yourself as a test user, configure redirect URI
    • Walk through Connect with Facebook → confirm a connection row appears
    • Trigger an auto-publish workflow → confirm a post lands on IG
    • Trigger an approval workflow → approve → confirm post lands on IG

🤖 Generated with Claude Code

The Instagram connection now goes through Facebook OAuth: users click
Connect with Facebook on /dashboard/connections, grant publishing and
insights scopes, and we resolve the IG Business Account ID via the Page
they pick. Username/password is gone from the schema and the UI. Tokens
are AES-GCM encrypted at rest with TOKEN_ENCRYPTION_KEY.

Posting moves to the Graph API two-step protocol: create media container,
then media_publish. Because the Graph API requires a public image URL,
every generated image is re-uploaded to Supabase Storage before publish
(both the auto-publish path inside the LangGraph agent and the approval
route on Vercel). DALL-E URLs and Gemini base64 are both normalized
through the same helper.

A weekly refresh-tokens cron exchanges each long-lived token for a fresh
one before its 60-day expiry. Expired tokens flip the connection to
status='expired' and the UI surfaces a Reconnect button.

Migration 004 deletes legacy username/password connections and replaces
graph_access_token with access_token_encrypted plus page_id,
token_expires_at, and connection_status. Workflows that referenced the
deleted connections lose their connection_id (ON DELETE SET NULL) and
need to be re-pointed; this is the documented breaking change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flowpost Ready Ready Preview, Comment May 8, 2026 1:48am

@Abby263 Abby263 merged commit 03615a7 into main May 8, 2026
15 checks passed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18d25eb66a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +57 to +58
const candidates = readCandidates();
const chosen = candidates?.find((c) => c.pageId === pageId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify OAuth page cookie belongs to signed-in user

POST /api/auth/instagram/select-page trusts ig_oauth_pages without checking that the cookie was created for the current account, even though the callback keeps ig_oauth_user for that purpose. In a shared browser/session-switch scenario (user A starts OAuth, user B signs in before cookie expiry), user B can select A’s pending page and bind A’s page token to B’s connection. Bind the pending payload to the current userId before accepting pageId.

Useful? React with 👍 / 👎.

Comment on lines +166 to +168
if (!accessToken || !igUserId) {
throw new Error(
"Instagram credentials missing accessToken or igUserId. Reconnect via Facebook OAuth.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve upload_post credential compatibility

This change makes upload_post require credentials.accessToken and credentials.igUserId, but existing callers like frontend/app/api/schedule-post/route.ts still pass connection.credentials directly, and new OAuth connections are saved with credentials = {} in the callback/select-page routes. As a result, Instagram “post now” (and any path using upload_post) fails with the new missing-credentials error until those callers are updated to pass decrypted OAuth fields.

Useful? React with 👍 / 👎.

Abby263 added a commit that referenced this pull request May 8, 2026
)

Mirrors the Instagram OAuth pattern from PR #11 for both X and LinkedIn:

X (Twitter):
- OAuth 2.0 PKCE at /api/auth/twitter/{start,callback}.
- Scopes: tweet.read, tweet.write, users.read, offline.access.
- Refresh tokens stored encrypted; auto-refreshed inline by
  agent-credentials.ts when within 5 min of expiry.
- TwitterOAuthClient.tweet does a bearer-auth POST /2/tweets, with optional
  v1.1 media/upload step for images.
- LinkedIn OAuth 2.0 (OIDC) at /api/auth/linkedin/{start,callback}.
- Scopes: openid, profile, w_member_social.
- 60-day access tokens; no refresh path → expired connections flip to
  status='expired' so the UI shows Reconnect.
- LinkedInOAuthClient.post does the 3-step UGC publish flow (registerUpload
  → PUT bytes → POST /v2/ugcPosts).

Migration 006 deletes legacy Twitter API-key rows and adds two columns:
refresh_token_encrypted (X only) and oauth_provider_user_id (Twitter user_id
for X, OIDC sub for LinkedIn — Instagram still uses ig_business_account_id).

Centralized credential prep in lib/agent-credentials.ts so trigger-workflow,
queue worker, and the inline refresh path stay consistent.

Connections UI replaces the API-key forms for X and LinkedIn with
"Connect with X" and "Connect with LinkedIn" buttons, matching IG's pattern.

upload-post graph rewritten to dispatch by platform with three new clients;
content-automation-advanced's publishToInstagram is renamed publishToPlatform
and routes by state.platform.

Refresh-tokens cron extended:
- Meta long-lived tokens: refresh within 14 days of expiry (unchanged).
- X: refresh within 1 day of expiry as a backstop (inline refresh handles
  in-use cases).
- LinkedIn: mark expired only; no refresh path exists.

SETUP.md gains Step 3.5 (X) and Step 3.6 (LinkedIn) with app registration
walkthroughs, redirect-URI configuration, and tier limitations.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@Abby263 Abby263 deleted the feat/instagram-oauth branch May 14, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant