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 IntegrationAdapter — startConnect, 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.