koder twillio cpaas — control-plane over a shared transport substrate
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 |
|---|---|---|
relay/adapters (Cloud API), bot-server channel, sign Twilio notifier |
3× | |
| SMS | talk Twilio bridge (sms.go), sign Twilio notifier (sms_twilio.go) |
2× |
| Telegram | bot-server channel, legacy kode-bridge |
2× |
| Voice / DID / IVR | kall (SIP trunk, DID provisioning, IVR flows) |
1× (control plane only) |
| Telemetry transport | karavan GT06 (LoRa backlog) |
1× |
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 aCapabilities()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.kmdQ2 — a pattern with ≥3 identified consumers mustlive 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) — donot 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.kmdD12 — cause-root > symptom — efforttimegating 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 acarrier 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, backedby the existing
specs/chat-channels/capability-matrix.kmd);Send → MessageIDretained for editreplydelete tracking;- one envelope (
transport.Message) merging relay's cleanIdentity/Body/Filewith bot-server's group
DirectlyAddressedcallback 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 IVR — delegated to
kall, whichalready ships these (
did.PhoneNumber,ivr.Flow,sip.TrunkConfig); - multi-tenant routing, per-channel capability gating, observability
(
trace_idper 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:
- relay#002 — define + adopt
transport.Channel; foldadapter/into it(the keystone package this RFC lays is the start). Foundation.
- bot-server#017 — converge
channel.Channelonto the substrate; drop thebespoke interface; reuse the shared WhatsApp/Telegram adapters.
- talk#063 — promote the Twilio SMS bridge to a
transport.ChannelSMSadapter; Talk consumes it.
- sign#069 — replace bespoke
sms_twilio.go/whatsapp_twilio.gowithsubstrate adapters (kills 2 dups).
- koder-stack#192 —
koder-twilliocontrol-plane service skeleton(unified API, router, channel registry, kall delegation for DID/IVR).
- 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.