# Social inbox (M6)

Only permitted surfaces (doc 05 §4). Today that means **comment threads on posts published through the platform**, for Facebook Pages, Instagram professional accounts, Threads profiles and YouTube channels. Direct messages stay `blocked`/`unavailable` in the capability data (Messenger review, Instagram Advanced Access, X paid tier). LinkedIn comments show as *configurable*: reading organisation comments needs the Community Management API, which is not approved yet.

**Threads** (reply management, scopes `threads_read_replies` + `threads_manage_replies`): replies at every depth come from `/{post}/conversation` and are filed under the top-level reply they descend from. Replying publishes a TEXT post with `reply_to_id`; hide/unhide uses `/{reply}/manage_reply`. The API gives usernames, not user ids. Threads is polled only (no webhook route yet).

## Model

- `conversations`: one per top-level comment (`external_thread_id`), linked to the published `post_targets` row; status open / pending (waiting on customer) / closed; assignee; tags.
- `messages`: the comment and its replies. `direction` in/out, `hidden`, `status` (received, sending, sent, failed, removed). Bodies are encrypted at rest.
- `inbox_notes` (internal, never sent), `saved_replies` (per client or global), `inbox_assignments` (history).
- `webhook_events` (tenant): dedupe and replay protection for provider webhooks.

## Ingestion

- **Polling** (`inbox:poll`, every 10 min): `SyncAccountInbox` jobs on the `sync` queue read comments on posts published in the last `INBOX_WINDOW_DAYS`. This requires the plan's `inbox` feature, an adapter implementing `ReadsInbox`, and the effective `read_comments` capability (integration readiness, flag, health, scopes).
- **Meta webhooks**: `GET/POST /webhooks/meta/events`. GET answers the subscription challenge with `META_WEBHOOK_VERIFY_TOKEN`. POST requires a valid `X-Hub-Signature-256`. Page `feed` comment changes and Instagram `comments` changes are routed through master `account_routes` (HMAC of provider + external account id, written when an account is linked; backfill with `inbox:sync-routes`) to the right tenant.
- Both paths go through `InboxIngestor`, which upserts by external id. Polling, webhooks and replays therefore converge on one row, and a new public comment reopens a closed thread.

## Actions (`InboxActions`)

| Action | Permission (on the conversation's client) | Platform requirement |
|---|---|---|
| Reply | `inbox.reply` | `reply_comments` capability (e.g. Facebook `pages_manage_engagement`, now requested at connect) |
| Hide / unhide | `inbox.moderate` | adapter supports `comments.hide` |
| Assign | `inbox.assign` (the assignee must have `inbox.reply`) | — |
| Status, tags, saved replies | `inbox.reply` | — |
| Notes | `inbox.view` | — |

A reply is stored as `sending` first. If the platform rejects it, it is kept as `failed` with the error. The platform's copy of a sent reply (same id) merges into the same row on the next poll.

## Retention

`inbox:prune` (daily) deletes closed conversations with no activity for `INBOX_RETENTION_DAYS` (365) and webhook receipts older than 30 days. Comments on deleted posts stop syncing when the post is marked deleted.
