# channels-scope-6 — Connections overhaul (sustainable, plain-named)

> Kickoff: Fable, 2026-07-22 evening. Operator: "the connectors and their
> names are too specific, and the section itself is not sustainable." This
> is the Channels & Connections scope's standing mandate (multi-channel
> connections + status surface) finally executed on the Settings →
> Connections section.

## The defects

1. **Names too specific / dishonest**: the `instagram` connection actually
   holds the FB Page token that powers Instagram AND Facebook (comments,
   DMs, posts). Legacy `meta`/provider names leak engineering vocabulary
   and the wrong scope. (Naming rule in memory: WhatsApp-specific
   meta→whatsapp; channel-agnostic → functional name, never "meta".)
2. **Not sustainable**: each connector is a bespoke hand-built card; adding
   a channel means a developer builds a new one — violates the
   self-sustaining law.

## The shape (orchestrator defaults — flag at gate if wrong)

- **Internal provider keys DO NOT change** (code + DB rows keep
  `instagram`, `whatsapp`, …) — renaming keys is a cross-codebase risk with
  zero operator value. What changes is the PRESENTATION layer, as data.
- **Connection metadata as seeded data**: display name (plain words),
  what-it-powers (channel chips: Instagram · Facebook · WhatsApp…),
  description, credential fields schema, health-check kind. One generic
  ConnectionCard renders ANY connection from its metadata — no bespoke
  per-provider components. Adding a future connector = a metadata row +
  credential fields, not a new screen.
- **Honest display names** (draft, operator reacts at gate): `instagram` →
  "Meta social (Instagram + Facebook)" — wait: naming rule forbids "Meta";
  use **"Social pages (Instagram + Facebook)"**; `whatsapp` → "WhatsApp";
  email/SMTP → "Email sending"; LeadRat → "CRM (LeadRat)"; n8n →
  "Workflows (n8n)". Plain words, what it does, never the vendor's
  API jargon.
- **Status read-back on every card** (self-rectifying law): token
  validity/expiry via the provider's cheap check (debug_token pattern),
  webhook subscription state where applicable, last-event-received time
  from webhook_events. Real state, never a hardcoded claim (the "rescope
  pending" lesson).
- Secrets stay in the encrypted store, edited in place, never displayed.

## Gate bars

1. Every existing connection renders through ONE generic card from
   metadata; grep-proof: no per-provider bespoke card component remains.
2. Names/chips plain + honest (the IG card shows Instagram AND Facebook).
3. Status is live read-back (token check + last webhook event time);
   pulling a token shows unhealthy honestly.
4. Suites green; typecheck/build; conformance test still green (tokens);
   review before gate; deploy + health; publish.

## Out of scope

Provider key renames in DB/code; new connectors; the cutover (scope-5).

## Stage log

- 2026-07-22: kickoff written; Seat I dispatched.

- **2026-07-22 (Seat I) — DELIVERED (build side), pushed to `rethink`:**

  **Metadata shape + where seeded** — `packages/shared/src/connections/presentation.ts`
  (`ConnectionPresentation`: `provider` [internal key, unchanged], `displayName`,
  `powers: ChannelId[]` [chips], `description`, `settings`/`secrets`
  [`ConnectionField[]` — the credential schema], `healthKind: "meta-webhook" | "none"`,
  optional `rotationNote`). These are the SEEDED DEFAULTS; the API serves them at
  `GET /api/connections/metadata` (`orderedConnectionPresentations()`). This
  replaced the web-layer `provider-schemas.ts` (deleted). Provider KEYS unchanged
  (no DB/enum churn), exactly as the shape section directed. **Flag for gate:** the
  registry lives in shared (served by the API), not a DB table — nothing in the
  defects needs operator-editable presentation, and this avoids a migration; flag if
  DB-seeded rows are required.

  **Draft display names (operator reaction requested):** whatsapp → **"WhatsApp"**;
  instagram → **"Social pages (Instagram + Facebook)"** (chips: Instagram + Facebook);
  llm → **"Language model (agent brain)"**; n8n → **"Workflows (n8n)"**; gmail →
  **"Email sending"**; leadrat → **"CRM (LeadRat)"**. No "Meta", no API jargon
  (pinned by a shared test).

  **One generic card** — `apps/web/src/components/settings/ConnectionCard.tsx` renders
  ANY connection from its metadata (name + channel chips + description + Active toggle
  + Test + Live-status). Removed all per-provider web code: `PROVIDER_SCHEMAS`,
  `PROVIDER_ORDER`, `WEBHOOK_PROVIDERS`, `UNTESTABLE`, the whatsapp-only `RotationNote`,
  and `ConnectionsTable`. **Grep-proof:** no `provider ===` / bespoke card branching
  in `apps/web/src/components/settings` (verified).

  **Live status read-back** — `webhook-status.service` gained a `token` block
  (debug_token pattern): WhatsApp inspects the access token with the app token in the
  Authorization header (app secret NEVER in a URL) → is_valid + expiry + scopes;
  Instagram (no app id stored) probes `GET /me?fields=id` → live/pulled, expiry an
  honest `unknown`. A pulled token reads `valid:false`. Combined with the existing
  subscription state + last-event-from-`webhook_events`, that is the per-card live
  status. No hardcoded claims.

  **Tests:** shared presentation 8 · api connections.controller 2 + webhook-status 20
  · web ConnectionCard 5 + ConnectionForm 4 + Settings 15 · conformance green; web
  build 0. Commits `7364e27` (shared), `201fe83` (api metadata + token), `14ced8e`
  (web generic card).

  **NOT done (gate remaining):** deploy + health (VPS/orchestrator lane — no local
  stack on this box); publish (orchestrator); operator reaction to the draft display
  names; a screenshot/live status read against the real Meta token.

- **2026-07-22 (Seat I) — review fixes (2 MAJOR + 1 minor), pushed:**
  - **MAJOR 1 (Test always hard-failed the Social-pages card):** added an
    `instagram` case to `connection-test.service` (`GET /me?fields=id,name` with the
    Page token), and added `testable` to the presentation registry — the card renders
    Test only when `meta.testable`, so the generic "Unknown provider" branch can never
    surface as a hard failure. Commits `c73fe3f` (shared), `a982da6` (api), `ba686ff`
    (web).
  - **MAJOR 2 (IG outage branded token "Invalid / pulled"):** `graphGet` now flags
    `authError` (401/403 or OAuthException/code 190); `instagramToken` reads
    `valid:false` ONLY on a genuine auth rejection, else `unknown` — matching the
    service contract + the WhatsApp arm. Replaced the conflation test; added IG 5xx +
    timeout → `unknown` tests. Commit `a982da6`.
  - **MINOR 3 (metadata failure → dead Edit buttons):** the Connections section now
    surfaces a metadata-fetch error + Retry in QueryState instead of unlabelled cards.
    Commit `ba686ff`.
  - Own-oracle: shared presentation 9 · api connection-test +3 / webhook-status 22 /
    controller 2 (45 across the 3 api files) · web ConnectionCard 6 / ConnectionForm 4
    / Settings 16 / conformance 5. api build 0; web typecheck + build 0.

## GATE VERDICT — PASSED (Fable, 2026-07-22 late)

Review (step 3) found 2 MAJOR + 1 minor; all fixed red→green same session
(instagram test case + metadata-driven Test visibility; auth-vs-transport
split so an outage reads "unknown" never "invalid"; metadata-error surfaced
with Retry). Own-run oracles: shared 9 · api 43 · web 22 green; deployed,
health {ok,db,redis}; published. Trailing (operator): reaction to draft
display names; one live "Live status" open on the Social-pages card
(real-token path — mocked-Graph-proven only).
