Projects

ProjectCommunications Infrastructure · 2026

Telephony

A provider-agnostic NestJS backend for SMS, voice, verification, lookup, phone numbers and A2P 10DLC. Product code talks to a capability, not a vendor SDK.

  • NestJS
  • TypeScript
  • Twilio
  • REST APIs
Product code calls a capability, a registry resolves the provider per capability, and inbound webhooks come back verifiedProduct codeno vendor SDKCapability tierone interface per capabilitysmsvoiceverifylookupnumbersProvider registryenv picks per capabilityRESOLVED PER REQUESTTwilio adapterthe only complete oneStub adaptersvonage · plivo · messagebirdInbound webhookssignature checked at the edgeduplicates assumedevent persisted firstCALLSRESOLVESPICKEDSTUBSINBOUNDSTATUS BACKONE OPERATION ID PER ACTIONACCEPTANCE UNKNOWN RECONCILED

Communications Infrastructure · Project

Chapter
Voice AI and telephony
Year
Availability
Private
Writing
3 related posts

Telephony is a NestJS backend that gives an application one API over its communications providers. SMS, voice, Verify, Lookup, phone numbers, messaging services and conversations each get a provider-neutral interface and controller. Before it, product code imported a vendor SDK directly, which meant the vendor was named in every feature that sent a message.

The obvious answer, wrapping the SDK in a service class, only moves the coupling one file deeper. The provider still leaks through its response shapes and its error codes, and switching one capability means touching everything that touched that service. I wanted the provider to be a runtime setting rather than an import.

# one provider per capability, resolved at request time pnpm start[Nest] TelephonyModule loaded[Nest] capability sms     -> twilio[Nest] capability voice   -> twilio[Nest] capability verify  -> twilio[Nest] capability lookup  -> twilio[Nest] adapters: twilio ready, vonage/plivo/messagebird stubs
Telephony in use

Capabilities not vendors

Each capability defines its own interface, and adapters register against it. Environment configuration chooses the provider separately for every capability, and a registry resolves that choice at request time. One deployment can therefore route voice through whichever carrier sounds best into a market, SMS through whoever holds the A2P registration, and verification through a third provider. Resolving per request is not automatic health-based failover, and I try not to describe it as one. The pattern came from running Twilio, Vonage and Telnyx behind one interface for a set of voice agents, then distilling what survived into this service.

Where portability stops

The classic way a provider abstraction fails is designing the interface around what every vendor has in common. You then cannot use the reasons you picked a vendor in the first place. A2P 10DLC registration, TrustHub, TaskRouter and subaccounts have provider-specific lifecycles that do not compress into a generic send method. So the layer is two tiers on purpose. The capability tier covers traffic that is genuinely portable, which is most of it. Alongside it, Twilio keeps first-class namespaced routes for A2P 10DLC, TaskRouter, TrustHub, Studio, Proxy, Fax, short codes, subaccounts, credentials, recordings, usage and webhooks. The dependency stays visible in one place rather than smeared through the codebase.

The inbound half

Sending through many providers is the easy direction. Every SDK send call maps onto a capability method. The hard direction is everything providers send back: message status webhooks, inbound SMS and call events, each with its own payload shape, retry behavior and signature scheme. Signatures are verified at the edge, using the exact URL, body or parsed parameters that scheme requires, before any business logic runs. Handlers assume duplicate and out-of-order delivery, persist the event before acknowledging it, and reconcile missing terminal states afterward. Status vocabularies are mapped explicitly, with the raw provider status kept beside the normalized event. An unfamiliar value becomes an explicit unknown state that alerts, never a silent default.

An ambiguous send

The dangerous call is the outbound request whose response never arrives. The carrier may already have accepted it. Retrying through a second provider then creates two real calls for one product action. So every product action carries a stable operation ID, and its provider, attempt and provider reference are persisted. Four outcomes are distinguished. An unsupported capability is rejected before any provider request. A confirmed rejection may fall through to an eligible alternative. An accepted operation is tracked through its lifecycle. Acceptance unknown is reconciled through status queries or callbacks before anything is issued again. A shared method name is easy. A shared meaning for “did we place the call?” is the part that took the work.

Decisions and costs

  • Two tiers rather than a lowest common denominator. Provider-specific routes stay first class, which means the layer does not promise portability it cannot deliver. The cost is that a workflow can quietly become Twilio-only.
  • One place for policy. Banned-number and do-not-call filtering, quiet hours and per-tenant rate limits live here rather than in each feature, which is also where calling-reputation hygiene belongs.
  • Ship stubs, not adapters. Vonage, Plivo and MessageBird exist as explicit stubs so an unimplemented capability is a stated fact rather than a runtime surprise. What I would revisit is shipping only one complete adapter, because a second real implementation is the only honest test of an abstraction.

Limits and status

Twilio is the one complete adapter today. Adding another means implementing the capability interfaces I care about and registering them, while the public routes and the product workflows above them stay where they are. Failover is bounded by reality rather than by code. The alternate provider still needs suitable numbers, credentials, capacity and registrations. Configuration cannot move an active call, port an inbound number instantly or transfer a campaign approval. With one provider, one use case and no volume, this whole layer is ceremony. It starts paying rent at the second provider, the second region, or the first outage.

Next projectAgentic Tools

Reckon

View project