agentleFS
bittencourtthuliobittencourtthulio/meta-graph-api-integration/skills/meta-graph-api/SKILL.md

meta-graph-api

Skill1 starsChanged 3 months agoMITView on GitHub

Implement or maintain a full Meta integration through the Graph API on a React + TypeScript + Supabase stack — Facebook OAuth for ad accounts, pages and Instagram accounts, campaign/adset/ad sync with insights, a campaign management tree with inline editing, end-to-end campaign creation, custom and lookalike audiences, pixels with the Conversions API, and the Instagram block (publishing, AI auto-post, DM inbox, keyword automation). Use when working on Meta Ads, Facebook, Instagram, Graph API tokens and scopes, ad sync, pixel events, or when a token expired, a scope is missing or a webhook stopped firing.

  • Reads credentials

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    App Secret never leaves the backend: Not in .env of the frontend, not in a component, not in a log.

    line 173
  2. Very specific:

    Use meta-implement to execute it, meta-debug when it breaks.

    line 205
  3. Distinctive:

    Never log them, never return them to the browser, encrypt at rest if your platform supports it.

    line 248

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Meta integration via Graph API

A complete Meta module — Facebook Ads plus Instagram — extracted from a production ad-management platform: 17 migrations, 30 edge functions, 17 hooks, 71 components, plus a six-sprint plan with acceptance gates.

The plan does not replicate the origin system's defects: it lists 18 concrete corrections (unified API version, missing GRANTs, absent foreign keys, anon policies leaking every tenant's rows, a DM webhook with no HMAC validation, no message deduplication…) and the artifacts implement them.

Reference docs are in Brazilian Portuguese (references/), UI strings in the components are pt-BR. Code identifiers are English.


0. Orient yourself before touching anything

# Is the module already here?
ls supabase/functions | grep -E "meta-|facebook-|instagram-"
grep -rl "facebook_integrations" supabase/migrations | head
  • Nothing → fresh implementation: use meta-implement, which walks the sprints with gates. The order is not negotiable — OAuth before sync, sync before creation.
  • Partially → find the last passed gate (§6) and resume there.
  • Broken → use meta-debug. Meta failures are almost always a token, a scope, or an ID belonging to the wrong account.
  • Just a question → the contract below usually answers it.

1. Architecture

FRONTEND (React + React Query)
  Settings → Meta: connection card · ad accounts · pages · IG accounts · scopes
  Client → Campaigns: campaign tree (campaign → adset → ad), inline edit, bulk
  Client → Audiences · Pixels · Auto-posts · DM inbox
        │ supabase-js (RLS)              │ invoke()
        ▼                                ▼
SUPABASE (Postgres)
  facebook_integrations · facebook_ad_accounts · facebook_pages
  instagram_accounts · campaigns · ad_sets · ads · sync_jobs
  facebook_sync_schedules · _execution_logs
  meta_audiences · meta_pixels · pixel_events_log
  instagram_auto_post_configs · _posts · _schedules · knowledge
  instagram_conversations · _messages · _flows · _triggers
        │
        ▼
EDGE FUNCTIONS (Deno) — all Graph calls go through _shared/meta-graph.ts
  meta-oauth · check-meta-scopes · fetch-facebook-pages
  sync-meta-campaigns (async job) · meta-sync-scheduler
  create-meta-campaign-full · create/update-meta-adset · -ad · manage-campaign
  manage-meta-audiences · manage-meta-pixels · send-pixel-event
  publish-instagram-post · instagram-post-scheduler · generate-instagram-auto-post
  instagram-messaging-webhook (PUBLIC, HMAC) · instagram-automation-executor
        │
        ▼
  🌐 Graph API v21.0

2. Graph API contract — get this right first

Base: https://graph.facebook.com/v21.0, as a single constant in _shared/meta-graph.ts. Never inline a version anywhere else — the origin system had five versions living side by side (v18 → v22), which is correction #1.

Three token types, and using the wrong one is the most common failure:

TokenWhere it livesUsed forExpiry
User tokenfacebook_integrations.access_tokenThe whole Ads pipeline~60 days (long-lived)
Page tokenfacebook_pages.page_access_tokenFB/IG publishing, DMsDoes not expire
App token{app_id}|{app_secret}debug_token only—

The OAuth exchange returns a short-lived token. You must exchange it again with grant_type=fb_exchange_token to get the 60-day one — the origin system skipped this and simply assumed 60 days (correction #3).

Content-Type: application/x-www-form-urlencoded on Marketing API creation POSTs; JSON elsewhere. This trips people up: a JSON body on /campaigns fails with an unhelpful error.

Endpoints in use

MethodEndpointPurpose
GET/oauth/access_tokencode → token, and short → long-lived
GET/debug_tokenValidate token, list granted scopes
GET/me/adaccounts · /me/accounts · /me/businessesDiscovery (paginated)
GET/{business}/owned_pages · /client_pages · /owned_instagram_accountsBusiness Manager assets
GET/{adAccount}/campaigns · /adsets · /adsSync, with nested insights{}
POST/{adAccount}/campaigns · /adsets · /adcreatives · /adsCreation
POST/{entityId}Update name, status, budget, targeting
POST/{entityId}/copiesDuplicate
GET/POST/DELETE/{adAccount}/customaudiences · /{audienceId}Audiences
GET/{adAccount}/adspixels · /{pixelId}/statsPixels
POST/{pixelId}/eventsConversions API
GET/search?type=adinterestInterest search
POST/{igUser}/media → /media_publishInstagram publishing (two steps)
GET/{igUser}/media · /{mediaId}/childrenIG feed and carousels
POST/me/messagesInstagram DM (Send API)

Attribution window on sync — omit it and your numbers will not match Ads Manager:

.action_report_time(conversion).action_attribution_windows(["7d_click","1d_view"])

Full request/response samples: assets/examples/graph-api-insights.md, criacao-campanha-requests.md, instagram-publish-requests.md, escopos-oauth.md.

3. Things about Meta that bite

  • Instagram publishing is two calls, not one: create a media container (/{igUser}/media), then publish it (/media_publish). Carousels are N containers plus one parent. A container expires in ~24h.
  • An adset's ad account must match its campaign's. Mixing them produces errors that name neither — the origin system needed a dedicated check-adset-account-mismatches function to find these.
  • insights{} comes back empty, not as an error, when the token lacks ads_read or the account has no delivery in the period. Empty ≠ broken.
  • Scopes are granted per user, not per app. debug_token tells you what was actually granted; never assume the requested list.
  • The DM webhook must validate x-hub-signature-256 (HMAC with the app secret). Without it, anyone can post fake messages into the inbox (correction #9).
  • The DM webhook redelivers. Deduplicate by message mid with a UNIQUE constraint, or a redelivery duplicates the message and re-fires the automation (correction #15).
  • redirect_uri is compared string for string between the auth dialog and the token exchange. One trailing slash apart and Meta returns "URL blocked".

4. Adapter points — what "any application" means

The artifacts came from a multi-tenant ad platform. Six integration points are host-app specific; everything else is self-contained.

Artifact expectsWhat it isHow to adapt
company_id + get_user_company_ids(uuid)Tenancy. A SECURITY DEFINER function used by every RLS policyYour tenant column and membership function (org_id, workspace_id). Single-tenant: keep one fixed tenant row rather than dropping the column
clients tableEach client points to 1 ad account + 1 FB page + 1 IG accountYour customer/project entity, or drop the level and link assets to the tenant
CompanyContext → activeCompany.company_idActive tenant in the browserYour equivalent context
useAuth, use-toast, cn()Auth, toasts, class mergeYour equivalents
VITE_FACEBOOK_APP_ID / FACEBOOK_APP_SECRETMeta app credentialsApp ID is a frontend env var; the secret is an Edge Function secret only — it must never reach the browser
OAUTH_ALLOWED_ORIGINS / APP_URLRedirect URI allowlistYour domains. Never reflect an arbitrary origin — that is an open redirect

The UI components assume shadcn/ui + Tailwind. Hooks and edge functions port unchanged to any UI kit.

5. Conventions that keep it working

  1. One Graph API version, in _shared/meta-graph.ts. Every call imports it.
  2. App Secret never leaves the backend. Not in .env of the frontend, not in a component, not in a log.
  3. RLS through the SECURITY DEFINER helper only. A direct subquery on an RLS-protected table inside a policy fails silently — empty array, no error.
  4. GRANT ... TO authenticated on every new table. The origin system had none, which is why data "disappeared" from the UI with no error (correction #2).
  5. No TO anon policies. A public dashboard is served by an Edge Function with the service role that resolves the token — not by opening the table (correction #13).
  6. verify_jwt = false only for Meta webhooks. Action endpoints stay authenticated (correction #8).
  7. Foreign keys declared, with ON DELETE CASCADE. ad_sets/ads in the origin had loose UUIDs (correction #6), and company_id was nullable, so a row written without a tenant vanished from the UI (correction #14).
  8. React Query for all server state — no useState mirrors of remote data.
  9. Never log a token, not even truncated. Log the entity id and the error code.

6. Roadmap (six sprints, each with a gate)

SprintDeliversGate
0 FoundationConnection + campaign schema, RLS, GRANTs, .env, config.tomlMigrations apply; SELECT returns empty, not a permission error
1 OAuth_shared/meta-graph.ts, oauth (with long-lived exchange), scope check, pagesConnect the account → ad accounts, pages and IG accounts appear
2 Sync & treeAsync sync job with insights, scheduler, campaign tree UINumbers match Ads Manager for the same period and attribution window
3 Create & editFull campaign creation, adset/ad create+update, builder wizard, editorsA campaign created from the app appears in Ads Manager
4 Audiences & pixelsCustom/lookalike audiences, pixels, Conversions API, interest searchAn event sent via CAPI shows in Events Manager
5 InstagramPublishing, AI auto-post, DM webhook + inbox, keyword automationA post publishes; a DM arrives in the inbox and fires its automation

Each sprint has one document per phase in references/, with tasks, artifacts and its own gate. references/ORQUESTRADOR.md is the master plan and lists all 18 corrections.

Use meta-implement to execute it, meta-debug when it breaks.

7. Failure modes worth memorizing

SymptomAlmost alwaysFix
Frontend gets [], no errorMissing GRANT or broken tenancy helperGrant to authenticated; check the helper
"URL blocked" on OAuthredirect_uri differs from the registered oneMatch exactly, including trailing slash
Token works, then stops after weeksShort-lived token was never exchangedgrant_type=fb_exchange_token
insights empty for every campaignMissing ads_read, or no delivery in the perioddebug_token to see granted scopes
Numbers do not match Ads ManagerAttribution window not setaction_attribution_windows(["7d_click","1d_view"])
Adset creation fails with a vague errorAdset's ad account ≠ campaign'scheck-adset-account-mismatches
IG publish returns a container id and nothing appears/media_publish was never calledTwo-step publish
DM arrives twice and automation fires twiceWebhook redelivery, no dedupUNIQUE on instagram_messages(external_id)
Webhook never firesNot subscribed, or verify_jwt left trueSubscribe the app; set false for the webhook only
Inbox needs a manual refreshTable not in the Realtime publicationALTER PUBLICATION supabase_realtime ADD TABLE … + REPLICA IDENTITY FULL

8. File map

SKILL.md                     this file
references/
  ORQUESTRADOR.md            master plan: scope, 18 corrections, contract, gates
  sprint-0…5/                one document per phase — tasks, artifacts, gate
assets/
  migrations/                17 SQL files, numbered in apply order (+ INDEX.md)
  edge-functions/            30 Deno functions + _shared/meta-graph.ts + config.toml
  frontend/hooks/            17 React Query hooks
  frontend/components/       71 components (connection, tree, builder, audiences,
                             pixels, Instagram, DM inbox)
  frontend/pages/            8 pages
  examples/                  Graph payloads, insights contract, OAuth scopes,
                             IG publishing, webhook payloads, .env.example

Companion skills: meta-implement (builds it, sprint by sprint) and meta-debug (isolates what broke).

9. Security and policy

  • App Secret is backend-only. A leaked secret lets someone impersonate your app against every user who authorized it.
  • User tokens are credentials with money attached — they can spend ad budget. Never log them, never return them to the browser, encrypt at rest if your platform supports it.
  • The DM webhook is a public, unauthenticated endpoint. HMAC validation is not optional.
  • Meta's Platform Terms apply to what you build. Ad data belongs to the advertiser; do not aggregate it across tenants. Production scopes require App Review, and automated messaging must respect Instagram's 24-hour messaging window. Building bulk unsolicited DMs is how apps get banned.
  • Pixel/CAPI payloads carry personal data. Emails and phones must be SHA-256 hashed, lowercase and trimmed, before they reach Meta.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.