# Kickoff — dedicated Instagram webhook endpoint + `meta`→`whatsapp` rename

> **Phase 1 of the rethink** (still). This packages a scoped, execute-ready task
> discovered mid-session on 2026-07-11: the shared `/webhooks/meta` endpoint
> cannot serve Instagram, because Instagram and WhatsApp are **different Meta
> apps** with **different verify tokens and app secrets**. This task makes
> real-time **webhooks** work the right way.
>
> **Superseded 2026-07-30 — do not resurrect the poller.** Comment webhooks are
> live in prod, so the standby that once sat on branch `feat/ig-comment-poller`
> is dead: the branch was **deleted** (operator's call), and its code was
> written against `packages/shared/src/meta/` before the module moved to
> `packages/shared/src/instagram/` — 833 commits of divergence, a rewrite, not
> a merge. If Meta ever breaks comment webhooks again, design a poller fresh
> against today's module; do not go looking for this one.
>
> Copy the prompt block below to start the executor session. The context section
> above it is the evidence that session should trust (verified live this session).

## Context the executor must trust (verified 2026-07-11, don't re-derive)

- **Two Meta apps, not one:**
  - **App A** = `1369718471663245` — the WhatsApp app. Its secrets (accessToken,
    appSecret, verifyToken) live in the **`meta` connection**. It currently owns
    the one shared endpoint `/webhooks/meta`.
  - **App B** = `1040420455120273` ("channels ING") — **owns Instagram**: it
    issued the IG Page access token, is connected to the Silver Oak Page
    (`109939868479359`), and manages the IG account
    **`17841444726857229`** (`@silveroakglobal.ae`). `debug_token` proved the IG
    token is **not** App A's.
- **Why the IG webhook has never fired:** the `instagram/comments` webhook +
  our callback were configured on **App A**, which doesn't own the IG
  connection; App B (which does) had **no** comments webhook. Events route to
  App B → nowhere. (Not purely an Advanced-Access wall — a wrong-app config.)
- **Why a shared endpoint can't be patched to "just work":**
  `apps/api/src/webhooks/webhook-ingest.service.ts` validates the `X-Hub`
  signature with **App A's** app secret (`MetaConfigService.getClient()`), and
  `apps/api/src/webhooks/webhooks.controller.ts` checks the verify token against
  the `meta` (App A) connection. App B events are signed with **App B's** secret
  → they land `signature_valid=false` → **stored but not processed**
  (`webhook-ingest.service.ts:114`). Different apps need different secrets.
- **The pipeline already exists and is reused, not forked:** the webhook
  processor already routes `object==="instagram"` →
  `IgCommentService.processEnvelope` (`webhook-processor.service.ts:55`). That
  service is idempotent by `commentId`, so **webhook and poller can coexist**
  without double-processing. This task only needs to *deliver a signed IG
  envelope into that existing service*.
- **Config store today:** the `instagram` connection holds `accessToken` +
  settings `{apiVersion:v25.0, igBusinessId:17841444726857229}`. It has **no**
  verify token and **no** App B app secret yet — both must be added.
  (`igBusinessId` was corrected this session from the Page id to the IG account
  id; if a rollback is ever needed the old value was `109939868479359`.)
- **App B's App Secret** is in App B's dashboard → App settings → Basic. The
  operator provides it; it is a real secret (store encrypted, never log/print).
- **Advanced Access is the open question.** Meta's docs say `comments` webhook
  delivery needs Advanced Access to `instagram_manage_comments` even for your own
  account; the operator disputes this by analogy to WhatsApp (which delivers for
  our own WABA on Standard Access). The finished endpoint is what settles it —
  configure App B's webhook, comment, and watch whether a row lands.

## Prompt — execute the IG webhook endpoint + rename (give to an Opus executor session)

```
Build a DEDICATED Instagram webhook endpoint and split Meta's per-app secrets
cleanly, then verify it live with a real comment. This is Phase 1 of the
rethink — read plans/rethink/ig-webhook-endpoint-kickoff.md first; its
"Context" section is verified fact from the 2026-07-11 session, trust it.

[SUPERSEDED 2026-07-21 — do not run this paragraph. The `fable-mode` skill is
retired and no seat spawns a Fable subagent; executors run Opus and STOP at an
uncertain fork. Current rules: KICKOFF-PROMPT.md + plans/rethink-execution.md.]
You are the EXECUTOR seat per plans/rethink-execution.md: at a fork you are not
confident about, do not guess. If a fork changes scope or contradicts the target
picture, stop and ask me instead.

Orient: CLAUDE.md (deploy loop, PID-cap test recipe — never bare pnpm test,
TDD, commit+push every verified step), then HANDOFF.md's 2026-07-11 entries
(the two-app webhook diagnosis + the poller fallback on branch
feat/ig-comment-poller). You have SSH to the VPS for deploy and live checks.
Work on the `rethink` branch (the rethink line; formerly feat/agent-unify).
The poller is parked on feat/ig-comment-poller (NOT running in prod) as the
standby — do not merge it, and don't rely on it running.

WHAT TO BUILD (TDD, additive, reuse — do not fork the ingest/processing path):
1. A dedicated Instagram webhook endpoint `/webhooks/instagram` (GET verify +
   POST ingest), EXCLUDED from the /api global prefix like /webhooks/meta
   (apps/api/src/main.ts exclude list). Its GET handshake validates against a
   NEW Instagram verify token, and its POST validates X-Hub-Signature against
   App B's app secret — BOTH read from the `instagram` connection, never the
   `meta` one. On a valid signature it must feed the SAME stored →
   webhook_events → processing path (object=instagram already routes to
   IgCommentService.processEnvelope) — reuse it, don't reimplement decisions.
   The right shape is likely: parameterize signature-source + verify-token by
   channel (an IgWebhookConfigService reading the instagram connection's
   verifyToken + appSecret), so the ingest service is no longer hard-wired to
   App A. Cross-module NestJS DI breaks are NOT caught by unit tests — rebuild
   + restart + /health per CLAUDE.md.
2. Store the two new IG secrets in the `instagram` connection: a verify token
   (you generate one; it is low-sensitivity, used only in the subscribe
   handshake) and App B's App Secret (ask me for it — from App B's dashboard,
   App settings → Basic). Both encrypted via the connections store; never in
   code or logs. Provide a safe way to set them (Settings → Connections UI
   field, or an operator-run command) — do not hardcode.
3. Rename the `meta` concept to `whatsapp` where it is WhatsApp-specific, and
   review/restructure the webhook code for the per-channel model the operator
   wants (whatsapp / instagram / facebook / website each with their own verify
   token + app secret + endpoint). This includes the `meta` CONNECTION provider
   → `whatsapp` (an additive, reversible data migration of the one row — note
   the CallbackAuthGuard reads the `n8n` connection, unaffected), MetaConfig
   service naming, and the shared MetaClient (it is the WhatsApp Cloud API
   client). Keep it additive and safe; if the rename's blast radius is large,
   split it into its own commit(s) and Fable-consult the sequencing. Do NOT
   break the live WhatsApp path — deploy-loop verify after.

VERIFY (the acceptance test):
- Per-package tests green (forks pool, maxForks=2 — never bare pnpm test);
  builds green; VPS deploy loop; /health {ok,db,redis}. No new destructive
  migrations expected (settings/connection edits are data, not schema).
- Route check: /webhooks/instagram GET returns the challenge only on the new
  IG verify token; POST with a body signed by App B's secret validates
  (signature_valid=true), a wrong signature does not.
- Then configure App B's Instagram webhook (I authorize the dashboard changes;
  drive my browser or hand me exact steps): callback /webhooks/instagram, the
  new verify token, Verify and save, subscribe the `comments` field.
- Ask me to comment on a Silver Oak IG post from a personal account; within a
  minute confirm a webhook_events row (object=instagram, signature_valid=true,
  processed=true), an ig_comments row, and a turn trace in /agent/activity with
  the post caption in its knowledge section. Screenshot the live trace.
- If the row lands but Advanced Access blocks delivery, that is the definitive
  answer to the WhatsApp-analogy question — report it plainly; the poller stays.

Standing rules: no eval runs; no secrets in code or logs (App B's app secret is
a real secret); additive/reversible only; update HANDOFF.md and commit+push each
verified step. Do not declare done or start other work: when verified, update
HANDOFF with evidence and hand back for the Fable gate. If webhooks deliver
end-to-end, the poller on feat/ig-comment-poller can then be retired — but that
is my call at the gate, not yours.
```

## Notes for whoever runs this

- The poller (`feat/ig-comment-poller`, built + verified but **parked** — NOT
  running in prod) and this webhook are two delivery mechanisms for the SAME
  idempotent pipeline. If webhooks work, the poller is simply dropped; if they
  stall, its branch is deploy-ready. Keep the two able to coexist.
- The `meta`→`whatsapp` rename is the channel-agnostic cleanup HANDOFF has
  flagged repeatedly. It is bundled here because the per-channel webhook split
  is the natural moment to do it — but it is the riskier half (touches the live
  WhatsApp path + a connection-provider migration). Treat it with the deploy
  loop's full rebuild+restart+health discipline and consider landing the IG
  endpoint first, rename second.
