koder twillio cpaas — control-plane over a shared transport substrate

draft

Status: Draft. This RFC ratifies the architectural form of the Koder Twillio CPaaS — the answer to backlog #169's "estende? absorve? camada acima?" — and lays the keystone contract as code. It does not build the whole platform; §7 slices that into phased tickets per regra 15-A (large implementation = slice + start, never defer).

1. Problem

The Stack already speaks to phones and chat networks — but every component reimplements its own transport, with no shared contract:

Capability Reimplemented in Count
WhatsApp relay/adapters (Cloud API), bot-server channel, sign Twilio notifier
SMS talk Twilio bridge (sms.go), sign Twilio notifier (sms_twilio.go)
Telegram bot-server channel, legacy kode-bridge
Voice / DID / IVR kall (SIP trunk, DID provisioning, IVR flows) 1× (control plane only)
Telemetry transport karavan GT06 (LoRa backlog)

Two near-identical interfaces sit at the center of the duplication:

  • relay.adapter.Adapter (services/foundation/relay/adapter/adapter.go:13)

    Name / Start / Stop / Send → MessageID / Subscribe → <-chan. Pull-model inbound. Shipped: Slack, Discord, WhatsApp. Stub: Signal, Matrix, Teams, iMessage, Google Chat.

  • bot-server.channel.Channel (core/bot-server/internal/channel/channel.go:10)

    Type / Start(inbound chan<-) / Send / Stop / Capabilities. Push-model inbound, plus a Capabilities() feature matrix relay lacks. Shipped v1.0.12: Telegram, WhatsApp, WebChat.

They overlap ~90%. The only real deltas are (a) pull-vs-push inbound, (b) bot-server's Capabilities() matrix, (c) relay's MessageID edit/reply tracking. The divergence is historical, not essential.

Backlog #169 ("Koder Twillio — VoIPURASMSRCSWhatsAppTelegramSignal/ LoRa") proposed a CPaaS "camada por cima" but left three questions open: naming, relationship to bot-serverrelay, and when to `k-new-product`. This RFC answers #1 (partial — §6) and #2 (fully — §3–§5).

2. Forces (the governing rules)

  • reuse-first.kmd Q2 — a pattern with ≥3 identified consumers must

    live in a shared contract, not per-caller. Transport has 5+ consumers (relay, bot-server, talk, sign, kall) already in tree. The shared contract is mandatory, not optional.

  • stack-principles.kmd §2 — Quality > Speed (absolute, 2026-05-29) — do

    not ship "v1 with limitations to fix later" when a structurally-correct form is reachable. A thin façade that calls each product's bespoke transport as-is is exactly that anti-pattern.

  • architecture-quality.kmd D12 — cause-root > symptom — efforttime

    gating do not lower a root fix. The root cause is *transport fragmentation; the fix is *one contract, not one more Twilio call site. Also weighed: D1 (correct abstraction), D2 (decoupling products from transport), D3 (new channel = new adapter, no core rewrite), D9 (reversibility — additive contract is a two-way door), D10 (reuse).

  • self-hosted-first.kmd — Twillio is a control-plane abstraction, not a

    carrier replacement: it still rides TwilioVonageTelnyx trunks upstream (kall already models TrunkConfig.Provider ∈ {twilio,vonage,generic}). No self-hosted-pairs gate applies to the abstraction layer itself; per-carrier self-hosting (e.g. an SMPP/SIP egress) is a separate, later pair.

3. Decision — two layers

Layer 1 — the substrate: a canonical transport contract

A single Go contract, transport.Channel, lives in the AI-free, API-consumed home that already owns channel IO: `servicesfoundationrelay`. It subsumes both existing interfaces:

  • inbound = relay's pull model (Subscribe() <-chan) — composes cleanly;

    bot-server's push is trivially wrappable onto it;

  • Capabilities() promoted from bot-server (single source of truth, backed

    by the existing specs/chat-channels/capability-matrix.kmd);

  • Send → MessageID retained for editreplydelete tracking;
  • one envelope (transport.Message) merging relay's clean Identity/Body/File

    with bot-server's groupDirectlyAddressedcallback semantics.

The keystone is laid as code by this RFC at services/foundation/relay/transport/ (additive — breaks nothing shipped). Existing transports converge onto it in phases (§7); they do not get rewritten, they get re-expressed against the shared contract.

Layer 2 — the control-plane: Koder Twillio

A thin service (services/foundation/, working slug koder-twillio — brand gated, §6) that owns no transport. It provides:

  • a unified send/receive API over any registered transport.Channel;
  • number/DID provisioning and IVRdelegated to kall, which

    already ships these (did.PhoneNumber, ivr.Flow, sip.TrunkConfig);

  • multi-tenant routing, per-channel capability gating, observability

    (trace_id per message), retry/backoff;

  • a registry of channels (chat via relay substrate, voice via kall, SMS via the

    promoted talk bridge, telemetry via karavan — out of CPaaS messaging scope but same registry pattern).

Products keep their domains: talk = E2EE messenger, kall = conferencing/voice, karavan = fleet telemetry. Twillio is the cross-channel CPaaS facade + router on top of the substrate — the control plane, not the data plane. This mirrors the layering the Stack already uses in gate-RFC-001-multi-channel-notifications (a producer fans out to pluggable notifiers).

4. Component role map

Component Today Under RFC-019
services/foundation/relay chat adapter framework owns transport.Channel; chat adapters re-expressed against it
core/bot-server own Channel iface converges onto transport.Channel (kills bespoke iface + dup WhatsApp/Telegram)
products/horizontal/talk local Twilio SMS bridge SMS becomes a transport.Channel adapter; Talk consumes it
products/horizontal/sign bespoke Twilio SMS+WhatsApp notifiers consumes substrate adapters (kills 2 dups)
products/horizontal/kall SIPDIDIVR control plane voice transport + DID/IVR provider that Twillio delegates to
products/vertical/karavan GT06; LoRa backlog telemetry transport (same registry; outside messaging scope)
koder-twillio (new) CPaaS control-plane: unified API + router + provisioning facade

5. The contract (laid as code)

See services/foundation/relay/transport/transport.go. Shape:

type Channel interface {
    ID() string                                              // "telegram","slack","whatsapp","sms","sip"
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
    Send(ctx context.Context, msg OutboundMessage) (MessageID, error)
    Subscribe(ctx context.Context) (<-chan InboundMessage, error)
    Capabilities() Capabilities
}

OutboundMessage{ChannelID, ReplyTo, Body}, InboundMessage{ID, ChannelID, From, Body, Group, DirectlyAddressed, Callback, Timestamp, Raw}, with Body/File/Identity from relay and Group/Callback from bot-server. Capabilities is the declarative matrix (specs/chat-channels/capability-matrix.kmd).

6. Naming (partial — owner-decision gate)

forms.kmd would slug a bare name "Twillio" → koder-twillio, display "Koder Twillio". But the bare name deliberately echoes the Twilio trademark, which #169 itself flagged ("evitar marca Twilio"). The architectural name is therefore not registered in component-names.md yet; the working slug koder-twillio is used in code/tickets pending an owner-decision on the public brand (candidates to weigh: a K-initial bare name → no koder- prefix, e.g. Komms / Konnect; or a functional service name). Tracked: koder-stack#193 (gated_by: owner-decision).

7. Build slices (phased tickets)

Per regra 15-A, the work is sliced — none deferred:

  1. relay#002 — define + adopt transport.Channel; fold adapter/ into it

    (the keystone package this RFC lays is the start). Foundation.

  2. bot-server#017 — converge channel.Channel onto the substrate; drop the

    bespoke interface; reuse the shared WhatsApp/Telegram adapters.

  3. talk#063 — promote the Twilio SMS bridge to a transport.Channel SMS

    adapter; Talk consumes it.

  4. sign#069 — replace bespoke sms_twilio.go / whatsapp_twilio.go with

    substrate adapters (kills 2 dups).

  5. koder-stack#192koder-twillio control-plane service skeleton

    (unified API, router, channel registry, kall delegation for DID/IVR).

  6. koder-stack#193 — public brand name resolution (`gated_by:

    owner-decision`, §6).

8. Alternatives rejected

  • A — thin façade over bespoke product APIs (leave fragmentation): fails

    D1/D12 — papers over the 3×WhatsApp/2×SMS duplication instead of fixing it; ossifies the very thing it should remove. Cheapest, structurally worst.

  • B — greenfield monolith (rebuild comms from scratch): fails D9/D10

    discards shipped bot-server v1.0.12, kall v1.4.0, talk v1.0.6, relay; max reversibility cost; violates reuse-first.

  • C — control-plane over shared transport substrate ✅ (this RFC): wins on

    D1D2D3D10D12; higher up-front cost (define the contract + migrate bot-server + collapse dups) is execution logistics, not a tiebreak metric (D12 explicitly bars effort from lowering a root fix).

Trade-off (one line): RFC-019 pays an up-front migration cost (one contract, bot-server convergence, dedup) to buy a Stack where adding a channel is one adapter and no product re-implements transport again — Quality > Speed + D12 mandate it over the cheaper façade that would freeze today's duplication.