Adapter catalogue

An adapter is one integration's implementation of the IntegrationAdapter contract in packages/types/src/capabilities.ts. It declares a key, a kind, a binding scope, the networks it serves, and which of the five capabilities it can honour on each. Nine are registered in apps/service/src/integrations/integrations.module.ts.

Key Kind Binding scope Networks Capabilities File
aggregator aggregator wildcard 11 social networks (see matrix) publish, messages, comments, reviews, connection — per network adapters/aggregator/aggregator.adapter.ts
instagram.native native owned-capabilities instagram messages, comments, connection adapters/meta/meta.adapter.ts
facebook.native native owned-capabilities facebook messages, comments, connection adapters/meta/meta.adapter.ts
whatsapp native owned-capabilities whatsapp messages, connection adapters/whatsapp/whatsapp.adapter.ts
whatsapp_session native owned-capabilities whatsapp_session messages, connection adapters/whatsapp-session/whatsapp-session.adapter.ts
sms native owned-capabilities sms messages, connection adapters/sms/sms.adapter.ts
email native owned-capabilities email messages, connection adapters/email/email.adapter.ts
livechat native owned-capabilities livechat messages, connection adapters/livechat/livechat.adapter.ts
ai native owned-capabilities ai messages, connection adapters/ai/ai.adapter.ts

Both Meta registrations share one Graph client and one MetaAdapter class, constructed twice with different keys. aggregator and the two Meta adapters are registered conditionally: without credentials configured they are absent in staging and production (a stub stands in only under development and test), so a connect against them fails with capability_unavailable rather than succeeding against a fake backend.

Network vocabulary

Two lists in packages/types/src/domain.ts. KNOWN_NETWORKS is their concatenation.

SOCIAL_NETWORKS (11) — an account is published to as well as messaged on: facebook, instagram, twitter, linkedin, tiktok, youtube, threads, bluesky, reddit, telegram, google_business.

MESSAGING_NETWORKS (6) — conversation-only, no feed and no comments: whatsapp, whatsapp_session, sms, email, livechat, ai.

Listing a network does not claim the platform can reach it. Each adapter declares its own networks and GET /v1/integrations/capabilities reports only what an adapter actually serves.

capabilityPossibleOnNetwork(capability, network) makes the messaging rule structural: on a messaging network only messages and connection are possible, so publish and comments are impossible whatever an adapter's own matrix says. Every adapter calls it inside supports() rather than each re-deriving (and eventually mis-deriving) the rule. With network omitted the answer is union semantics — "supported on at least one network".

whatsapp_session being its own network rather than a variant of whatsapp is deliberate: different connect flow, different credentials, different payload shape. See WhatsApp.

The aggregator matrix

AGGREGATOR_NETWORKS is written out explicitly, not aliased to KNOWN_NETWORKS — a network added for a native-only integration must not silently become an aggregator claim. Publishing covers every network it serves; the inbox add-on does not.

Set Networks
AGGREGATOR_NETWORKS all 11 social networks
DM_NETWORKS facebook, instagram, twitter, bluesky, reddit, telegram
COMMENT_NETWORKS facebook, instagram, twitter, bluesky, threads, reddit, youtube, linkedin, tiktok
REVIEW_NETWORKS google_business

So aggregatorSupports('messages', 'linkedin') is false, and a LinkedIn connection asked to serve DMs gets capability_unavailable at resolve time rather than a confusing upstream failure. google_business is reviews-only: no DMs, no comments.

Providers inside an adapter

Vendor names never appear in the service layer — they live inside one adapter's own files, keyed off the credential's provider field. Three adapters are multi-vendor:

Adapter Provider keys Notes
whatsapp gupshup, zernio, infobip gupshup and zernio are both Cloud API proxies sharing one CloudApiProvider; infobip has its own
sms africastalking, infobip, tiara
whatsapp_session wasender, openwa DEFAULT_SESSION_PROVIDER = 'wasender' — a row with no provider is a wasender session

Sending resolves providers.get(cred.provider) and throws when no provider is registered for that key, so a typo in an imported credential fails loudly rather than sending nothing.

Connect-time routing

ROUTING_TABLE in integration.registry.ts picks the default integration for a (network, capability) pair. Anything not listed falls through to aggregator.

instagram        messages -> instagram.native
facebook         messages -> facebook.native
whatsapp         messages -> whatsapp
whatsapp_session messages -> whatsapp_session
sms              messages -> sms
email            messages -> email
livechat         messages -> livechat
ai               messages -> ai

effectiveIntegrationKey(network, capability, override?) applies precedence: caller override, then the routing table, then a guard — if the chosen key is not registered or cannot serve the network, it falls back to aggregator.

The messaging entries exist for a sharp reason. The aggregator has no WhatsApp, email or AI support at all. Without the explicit row the default would route messages to aggregator, ownedCapabilities would filter the slot out, and no binding would be written — the connection lands looking connected and reports "has not been set up" on every send.

Adding a channel

One adapter file. Threads, dedup, unread counts, realtime fan-out, webhook retries and outbound retries are all channel-agnostic and sit above the adapter boundary, so a new integration implements IntegrationAdapterstartConnect, completeConnect, disconnect, verifyWebhook, verifyChallenge, normalizeWebhook, plus whichever provider objects it serves — declares its connectBindingScope and networks, and gets registered in integrations.module.ts. If it needs a non-default connect route, add a ROUTING_TABLE entry. Nothing in the inbox, ticketing, campaign or flow layers changes.