Integration adapters
What this is: the pattern by which the platform talks to any external vendor — wearables (Dexcom, Oura, WHOOP, Libre, Junction), labs (Junction Labs), telehealth (Rimo), email (Resend, Postmark), SMS (Twilio), payments (Stripe). Every external vendor has the same shape: one adapter package, one gateway service, registered through a provider registry.
Who it’s for: anyone adding a new vendor, debugging a flaky third-party, or trying to understand why @platform/integrations-adapter-junction and @platform/integrations-adapter-junction-labs coexist (one vendor, two capabilities — see Shared vendor, multiple capabilities).
What to read next: Service architecture, Events, Rate limits and circuit breakers.
Source ADR: 0042 — Channel adapters. Related: 0044 — Contacts provider registry, 0051 — Payment provider abstraction.
The rule (Architecture Decision Record 0042)
A vendor integration is a package (
packages/<channel>-adapter-<vendor>/) imported by one gateway service. Apps never import adapters directly — they call the SDK, which calls the service, which calls the adapter.
This gives us:
- Swappable vendors. Stripe → Adyen is a config change, not a refactor — the service holds the adapter contract; the adapter implements the vendor specifics.
- One audit point. The service owns auth, audit logs, retries, and circuit breakers; adapters stay focused on protocol translation.
- Testable in isolation. Adapters take a
fetchin their constructor and can be tested with mocked HTTP. The service is tested against an in-memory adapter.
Anatomy of an adapter
A vendor adapter exposes:
- A client class (for example,
JunctionLabsClient,RimoAdapterClient) — protocol translation, typed methods, vendor-specific errors mapped to a standard hierarchy. - Zod schemas and TypeScript types — the on-the-wire shapes, re-used by the service for OpenAPI generation.
- Webhook utilities — signature validation, timestamp replay-window check, payload parsing into a discriminated union of event types.
- Mapping helpers — translate vendor shapes into platform-shaped contracts (for example,
mapJunctionLabResultToRawBiomarkers).
What an adapter never contains:
- HTTP route handlers (those live in the gateway service)
- Database access (the service owns the schema)
- Audit log writes (the service writes them around the adapter call)
- Cross-vendor business logic (each adapter is single-vendor)
Channels and their gateway services
| Channel | Gateway service | Adapter packages |
|---|---|---|
| Wearables | services/integrations | integrations-adapter-{junction,dexcom,oura,whoop,libre} |
| Bloodwork | services/clinical | integrations-adapter-junction-labs |
| Telehealth | services/integrations | integrations-adapter-rimo |
services/comms | comms-adapter-{resend,postmark} | |
| SMS | services/comms | comms-adapter-twilio |
| Web push | services/comms | comms-adapter-web-push |
| Marketing | services/comms | comms-adapter-klaviyo |
| Payments | services/payments | (provider abstraction per ADR-0051) |
A channel can hold many adapters. A new vendor for an existing channel adds an adapter package. A new channel adds a contracts package and either a new gateway service or an extension of an existing one.
Shared vendor, multiple capabilities
Junction (Vital) provides both wearables and bloodwork. The two capabilities share a vendor account but are otherwise independent: different endpoints, different webhooks, different audit needs, different gateway services. The platform splits them:
@platform/integrations-adapter-junction— wearables, bundled intoservices/integrations@platform/integrations-adapter-junction-labs— bloodwork, bundled intoservices/clinical
Split whenever two capabilities share only a vendor account but differ in:
- Contracts (lab-order versus activity-summary)
- Consumers (clinical staff versus the wearables sync cron)
- Failure-mode handling (lab-order retry semantics differ from wearable backfill)
A conditional like if (capability === "labs") … inside an adapter is the signal that the split has not been made yet.
Webhooks
Webhooks are the inbound half of the adapter contract. Every adapter exports:
verify<Vendor>WebhookSignature(rawBody, header, secret)— hash-based message authentication code (HMAC) checkvalidate<Vendor>WebhookTimestamp(headers, toleranceSeconds?)— replay-window checkparse<Vendor>WebhookEvent(payload)— returns a discriminated-union event
Gateway services apply a requireScope or signature middleware before invoking the parser. The parsed event is republished as a platform event (vendor.capability.thing.v1) and other services subscribe through EventBridge. No service imports another vendor’s adapter to react to an event.
When not to reach for an adapter
- Internal services calling internal services. Use the typed SDK package (
@platform/sdk-<service>), not an adapter. - One-off scripts. Hit the vendor API directly from a script if the call lives nowhere else. Adapters exist to keep production code uniform; ad-hoc tooling does not need that uniformity.
- Vendor SDKs that already provide a strong contract. Re-export them with a thin wrapper rather than re-implementing the type surface.
Common mistakes
- Calling an adapter from another service. Call the gateway service over HTTP, or subscribe to its republished event.
- Letting an adapter throw vendor errors directly to the route. Map them to a platform
ErrorCodeat the service boundary so OpenAPI consumers see a stable error shape. - Skipping the idempotency key on outbound vendor calls. Junction, Stripe, and similar vendors deduplicate by idempotency key. Use
@platform/idempotencyto generate one and pass it through. - Putting brand-specific logic in the adapter. Brand routing belongs in the service per Brands and multi-tenancy.
Reference implementations
@platform/integrations-adapter-rimo— telehealth single sign-on plus webhook handling@platform/integrations-adapter-junction-labs— bloodwork orders plus webhook handling
See also
- ADR: 0042 — Channel adapters
- ADR: 0044 — Contacts provider registry
- ADR: 0051 — Payment provider abstraction
- Modular plan: Adapter packages
- Related concepts: Service architecture, Rate limits and circuit breakers
- Runbooks: Rimo webhook, Junction Labs