Skip to main content
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.
SurfaceSource-of-truth tableDispatcherPurpose
webhookwebhook_endpoints (direction=inbound)routes/external_webhooks.py + routes/alertmanager_webhook_routes.pyExternal SaaS sends an event in (Lemon Squeezy, Resend, Alertmanager) via dedicated routes.
outboundwebhook_endpoints (direction=outbound)services/integrations/outbound_dispatcher.pyWe send an event out (Discord, Telegram, Vercel ISR purge).
tapexternal_tapsservices/integrations/tap_runner.pyPull external data on a schedule (HackerNews, Dev.to, GA4, GSC, Singer subprocesses).
retentionretention_policiesservices/integrations/retention_runner.pySweep aging data — TTL-prune, downsample, summarize-to-table.
publishingpublishing_adaptersservices/jobs/media_distribute.pyDistribute rendered media (podcast / video) to platforms (YouTube, Postiz).

Handler catalog

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:
  • Alertmanageralert_eventsroutes/alertmanager_webhook_routes.py (POST /api/webhooks/alertmanager)
  • Lemon Squeezyrevenue_eventsroutes/external_webhooks.py (POST /api/webhooks/lemon-squeezy)
  • Resendsubscriber_eventsroutes/external_webhooks.py (POST /api/webhooks/resend)
The webhook surface stays available for third-party plugin handlers, which register via services.integrations.register_handler.
Poindexter sends events to external services. Outbound rows route through a single dispatcher; the per-handler doc covers retry and rate-limit behavior.
Scheduled pulls of external data into Postgres. Taps are scheduled by PluginScheduler from rows in external_taps.
Sweep aging data. Each policy is a row in retention_policies keyed on table + TTL + handler.
  • retention.ttl_prune — drop rows older than the row’s ttl_days
  • 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)
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.
External service setup that doesn’t fit any single handler doc.

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.
1

Write the handler module

Drop a new module under services/integrations/handlers/ (e.g. publishing_linkedin.py) and register it.
2

Import at startup

Add the module to services/integrations/handlers/__init__.py:load_all() so it’s registered when the worker boots.
3

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.
4

Document the handler

Add docs/integrations/publishing_linkedin.md mirroring the existing per-handler doc structure (config, secrets, contract, runbook).
5

Enable it

poindexter publishers list shows the new row; poindexter publishers enable linkedin_main activates it.