> ## Documentation Index
> Fetch the complete documentation index at: https://gladlabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrations

> Per-handler operator documentation for Poindexter's declarative integrations framework.

Every handler registered under `services.integrations` has a dedicated doc
explaining what external system it talks to, the config fields on the
integration row, the secrets it reads from `app_settings`, the expected
payload contract, and the operator runbook (add, test, rotate, disable).

The shared design is summarized below: one source-of-truth table per
surface, handlers registered as `<surface>.<name>`, encrypted secrets
resolved through a single audited path, and `enabled=false` as the safe
default for every row.

## Surfaces

The integrations registry namespaces handlers by surface so the same short
name can be reused across surfaces. Five surfaces are in flight today.

| Surface      | Source-of-truth table                      | Dispatcher                                                              | Purpose                                                                                     |
| ------------ | ------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `webhook`    | `webhook_endpoints` (direction=`inbound`)  | `routes/external_webhooks.py` + `routes/alertmanager_webhook_routes.py` | External SaaS sends an event in (Lemon Squeezy, Resend, Alertmanager) via dedicated routes. |
| `outbound`   | `webhook_endpoints` (direction=`outbound`) | `services/integrations/outbound_dispatcher.py`                          | We send an event out (Discord, Telegram, Vercel ISR purge).                                 |
| `tap`        | `external_taps`                            | `services/integrations/tap_runner.py`                                   | Pull external data on a schedule (HackerNews, Dev.to, GA4, GSC, Singer subprocesses).       |
| `retention`  | `retention_policies`                       | `services/integrations/retention_runner.py`                             | Sweep aging data — TTL-prune, downsample, summarize-to-table.                               |
| `publishing` | `publishing_adapters`                      | `services/jobs/media_distribute.py`                                     | Distribute rendered media (podcast / video) to platforms (YouTube, Postiz).                 |

## Handler catalog

<AccordionGroup>
  <Accordion title="webhook surface (inbound)">
    No first-party inbound handlers ship today. The live inbound webhooks are
    served by dedicated FastAPI routes that verify the signature, normalize the
    payload, and write to the landing table directly:

    * **Alertmanager** → `alert_events` — `routes/alertmanager_webhook_routes.py` (`POST /api/webhooks/alertmanager`)
    * **Lemon Squeezy** → `revenue_events` — `routes/external_webhooks.py` (`POST /api/webhooks/lemon-squeezy`)
    * **Resend** → `subscriber_events` — `routes/external_webhooks.py` (`POST /api/webhooks/resend`)

    The `webhook` surface stays available for third-party plugin handlers, which
    register via `services.integrations.register_handler`.
  </Accordion>

  <Accordion title="outbound surface">
    Poindexter sends events to external services. Outbound rows route
    through a single dispatcher; the per-handler doc covers retry and
    rate-limit behavior.

    * [`outbound.apprise_notify`](/docs/integrations/outbound_apprise) — generic push delivery (Discord, Telegram, ntfy, Slack, …) via Apprise
    * [`outbound.vercel_isr`](/docs/integrations/outbound_vercel_isr) — Vercel ISR cache revalidation
  </Accordion>

  <Accordion title="tap surface">
    Scheduled pulls of external data into Postgres. Taps are scheduled by
    PluginScheduler from rows in `external_taps`.

    * [`tap.builtin_topic_source`](/docs/integrations/tap_builtin_topic_source) — first-party TopicSources (HackerNews, Dev.to, codebase, web\_search, knowledge, dev\_diary)
    * [`tap.external_metrics_writer`](/docs/integrations/tap_external_metrics_writer) — write Singer-tap output into `external_metrics`
    * [`tap.singer_subprocess`](/docs/integrations/tap_singer_subprocess) — generic Singer-protocol subprocess runner (GA4, GSC)
  </Accordion>

  <Accordion title="retention surface">
    Sweep aging data. Each policy is a row in `retention_policies` keyed
    on table + TTL + handler.

    * [`retention.ttl_prune`](/docs/integrations/retention_ttl_prune) — drop rows older than the row's `ttl_days`
    * [`retention.downsample`](/docs/integrations/retention_downsample) — aggregate raw rows into a coarser rollup table, then delete the originals
    * `retention.summarize_to_table` — LLM-based summarization of aging rows (handler implemented; doc pending)
  </Accordion>

  <Accordion title="publishing surface">
    Distribute rendered media to platforms. Adapters are rows in
    `publishing_adapters` dispatched by `services/jobs/media_distribute.py`;
    adding a new platform is an insert plus a `publishing.<name>` handler
    registration.

    * `publishing.youtube` — upload rendered video to YouTube (per-handler doc pending)
    * `publishing.postiz_video` — push rendered video to platforms via Postiz (per-handler doc pending)

    Text social copy (X, Bluesky, Mastodon, LinkedIn) is a *separate* path: the
    `social.generate_drafts` atom writes `social_post_drafts` rows that are
    approved (`poindexter social approve`) and posted through Postiz — not the
    publishing surface.
  </Accordion>

  <Accordion title="Setup guides">
    External service setup that doesn't fit any single handler doc.

    * [Google Search Console + Analytics 4 setup](/docs/integrations/setup-gsc-and-ga4) — provision the credentials the tap subprocesses need.
  </Accordion>
</AccordionGroup>

## Adding a new platform

The point of the declarative-data-plane is that adding a new social platform
equals an insert plus a `<surface>.<name>` handler registration. No edit to
the dispatcher.

<Steps>
  <Step title="Write the handler module">
    Drop a new module under `services/integrations/handlers/`
    (e.g. `publishing_linkedin.py`) and register it.

    ```python theme={null}
    from services.integrations.registry import register_handler

    @register_handler("publishing", "linkedin")
    async def linkedin_post(payload, *, site_config, row, pool):
        ...
    ```
  </Step>

  <Step title="Import at startup">
    Add the module to `services/integrations/handlers/__init__.py:load_all()`
    so it's registered when the worker boots.
  </Step>

  <Step title="Seed the adapter row">
    Add a migration that inserts the `publishing_adapters` row — copy the
    `INSERT INTO publishing_adapters` shape from the rows seeded in
    `services/migrations/0000_baseline.py`.
  </Step>

  <Step title="Document the handler">
    Add `docs/integrations/publishing_linkedin.md` mirroring the existing
    per-handler doc structure (config, secrets, contract, runbook).
  </Step>

  <Step title="Enable it">
    `poindexter publishers list` shows the new row;
    `poindexter publishers enable linkedin_main` activates it.
  </Step>
</Steps>
