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
In this repository2 other files it gives its agents.
- meta-debugSkill
- meta-implementSkill
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
- Very specific: line 173
App Secret never leaves the backend: Not in .env of the frontend, not in a component, not in a log.
- Very specific: line 205
Use meta-implement to execute it, meta-debug when it breaks.
- Distinctive: line 248
Never log them, never return them to the browser, encrypt at rest if your platform supports it.
≈ “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.
Files
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.02. 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:
| Token | Where it lives | Used for | Expiry |
| User token | facebook_integrations.access_token | The whole Ads pipeline | ~60 days (long-lived) |
| Page token | facebook_pages.page_access_token | FB/IG publishing, DMs | Does 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
| Method | Endpoint | Purpose |
| GET | /oauth/access_token | code → token, and short → long-lived |
| GET | /debug_token | Validate token, list granted scopes |
| GET | /me/adaccounts · /me/accounts · /me/businesses | Discovery (paginated) |
| GET | /{business}/owned_pages · /client_pages · /owned_instagram_accounts | Business Manager assets |
| GET | /{adAccount}/campaigns · /adsets · /ads | Sync, with nested insights{} |
| POST | /{adAccount}/campaigns · /adsets · /adcreatives · /ads | Creation |
| POST | /{entityId} | Update name, status, budget, targeting |
| POST | /{entityId}/copies | Duplicate |
| GET/POST/DELETE | /{adAccount}/customaudiences · /{audienceId} | Audiences |
| GET | /{adAccount}/adspixels · /{pixelId}/stats | Pixels |
| POST | /{pixelId}/events | Conversions API |
| GET | /search?type=adinterest | Interest search |
| POST | /{igUser}/media → /media_publish | Instagram publishing (two steps) |
| GET | /{igUser}/media · /{mediaId}/children | IG feed and carousels |
| POST | /me/messages | Instagram 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-mismatchesfunction to find these. insights{}comes back empty, not as an error, when the token lacksads_reador the account has no delivery in the period. Empty ≠ broken.- Scopes are granted per user, not per app.
debug_tokentells 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
midwith a UNIQUE constraint, or a redelivery duplicates the message and re-fires the automation (correction #15). redirect_uriis 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 expects | What it is | How to adapt |
company_id + get_user_company_ids(uuid) | Tenancy. A SECURITY DEFINER function used by every RLS policy | Your tenant column and membership function (org_id, workspace_id). Single-tenant: keep one fixed tenant row rather than dropping the column |
clients table | Each client points to 1 ad account + 1 FB page + 1 IG account | Your customer/project entity, or drop the level and link assets to the tenant |
CompanyContext → activeCompany.company_id | Active tenant in the browser | Your equivalent context |
useAuth, use-toast, cn() | Auth, toasts, class merge | Your equivalents |
VITE_FACEBOOK_APP_ID / FACEBOOK_APP_SECRET | Meta app credentials | App ID is a frontend env var; the secret is an Edge Function secret only — it must never reach the browser |
OAUTH_ALLOWED_ORIGINS / APP_URL | Redirect URI allowlist | Your 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
- One Graph API version, in
_shared/meta-graph.ts. Every call imports it. - App Secret never leaves the backend. Not in
.envof the frontend, not in a component, not in a log. - RLS through the
SECURITY DEFINERhelper only. A direct subquery on an RLS-protected table inside a policy fails silently — empty array, no error. GRANT ... TO authenticatedon every new table. The origin system had none, which is why data "disappeared" from the UI with no error (correction #2).- No
TO anonpolicies. A public dashboard is served by an Edge Function with the service role that resolves the token — not by opening the table (correction #13). verify_jwt = falseonly for Meta webhooks. Action endpoints stay authenticated (correction #8).- Foreign keys declared, with
ON DELETE CASCADE.ad_sets/adsin the origin had loose UUIDs (correction #6), andcompany_idwas nullable, so a row written without a tenant vanished from the UI (correction #14). - React Query for all server state — no
useStatemirrors of remote data. - Never log a token, not even truncated. Log the entity id and the error code.
6. Roadmap (six sprints, each with a gate)
| Sprint | Delivers | Gate |
| 0 Foundation | Connection + campaign schema, RLS, GRANTs, .env, config.toml | Migrations apply; SELECT returns empty, not a permission error |
| 1 OAuth | _shared/meta-graph.ts, oauth (with long-lived exchange), scope check, pages | Connect the account → ad accounts, pages and IG accounts appear |
| 2 Sync & tree | Async sync job with insights, scheduler, campaign tree UI | Numbers match Ads Manager for the same period and attribution window |
| 3 Create & edit | Full campaign creation, adset/ad create+update, builder wizard, editors | A campaign created from the app appears in Ads Manager |
| 4 Audiences & pixels | Custom/lookalike audiences, pixels, Conversions API, interest search | An event sent via CAPI shows in Events Manager |
| 5 Instagram | Publishing, AI auto-post, DM webhook + inbox, keyword automation | A 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
| Symptom | Almost always | Fix |
Frontend gets [], no error | Missing GRANT or broken tenancy helper | Grant to authenticated; check the helper |
| "URL blocked" on OAuth | redirect_uri differs from the registered one | Match exactly, including trailing slash |
| Token works, then stops after weeks | Short-lived token was never exchanged | grant_type=fb_exchange_token |
insights empty for every campaign | Missing ads_read, or no delivery in the period | debug_token to see granted scopes |
| Numbers do not match Ads Manager | Attribution window not set | action_attribution_windows(["7d_click","1d_view"]) |
| Adset creation fails with a vague error | Adset's ad account ≠ campaign's | check-adset-account-mismatches |
| IG publish returns a container id and nothing appears | /media_publish was never called | Two-step publish |
| DM arrives twice and automation fires twice | Webhook redelivery, no dedup | UNIQUE on instagram_messages(external_id) |
| Webhook never fires | Not subscribed, or verify_jwt left true | Subscribe the app; set false for the webhook only |
| Inbox needs a manual refresh | Table not in the Realtime publication | ALTER 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.exampleCompanion 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.
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
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.

