ADR-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack.
Supersedes ADR-0003 (Rust-native trunk SIP). The 2026-06 strategic-relevance
review stress-tested ADR-0003 against the 2026 market + solo-build realities
and it did not survive: a trunk SIP/media core is the highest-cost,
lowest-differentiation square on the board (the perceived-quality battle and
the buying criteria both live above the transport), and no production-hardened
pure-Rust SIP stack exists to stand on (ezk is bus-factor-1 / pre-1.0;
str0m self-describes as 'not for production' for SIP).
rutster owns no SIP stack. PSTN reach is rented transport in three layers:
1. Primary (demo, most users) — CPaaS raw-media fork (Twilio Media Streams,
Telnyx). Media-leg ingress, core-as-server (parallel to WebRTC ingress
per ADR-0006). Use the raw-audio fork, *not* managed Voice-AI products
that would consume the reflex loop.
2. Graduation (on-prem) — out-of-tree SBC (Kamailio/FreeSWITCH/drachtio +
rtpengine) B2BUAs carrier SIP into rutster as clean RTP/tap media,
outside the trust boundary.
3. Never — a first-party Rust trunk SIP stack. rsip/ezk stay off the
critical path.
The only things rutster owns in Rust: the call model, the reflex loop, the
agent tap. Everything that touches a carrier is rented (layer 1) or
out-of-tree (layer 2).
ADR-0008 — The FOB and the green zone: the build-vs-reuse doctrine. Names
the boundary criterion implicit across ADRs 0002-0007 as one mechanical rule
every contributor (human or agent) applies the same way.
- FOB (build in Rust) — admitted only if it passes one of: hot path,
security-constitutive, differentiating. Current FOB: media termination
(rutster-media on str0m) · real-time reflexes (VAD/barge-in/jitter/pacing)
· call model (rutster-call-model) · agent tap (rutster-tap) · spend/abuse
gate (rutster-spend) · control API + state/bus trait (rutster).
- Green zone (reuse at arm's length) — its own process/container/trust
domain, never in the FOB's address space. Admitted when it fails all
three FOB tests AND a trusted, actively-maintained project already does
it well. Current green zone: Valkey (ADR-0005) · carrier trunk (ADR-0007)
· agent brain · reference GUI · object storage · KMS · OTel collector ·
container supervisor.
- 'Actively maintained' is the load-bearing gate: excludes a first-party
Rust SIP stack and webrtc-rs; admits Valkey and str0m-for-WebRTC.
- When in doubt, default to green zone. The FOB earns its members.
Restates the memory-safety pillar precisely: the FOB is 100% memory-safe
Rust; the green zone is trusted battle-tested OSS kept outside the boundary
— not an over-claim that every byte is Rust.
ADR-0003's status is flipped to Superseded with a forward-pointer to ADR-0007.
Kept as the historical record of the 'own trunk SIP + media termination in
Rust, no SBC shield' plan; reversed under the strategic-relevance review.
9.7 KiB
ADR-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack
- Status: Accepted
- Date: 2026-06
- Supersedes: ADR-0003 — Rust-native trunk SIP, no SBC shield
- Origin: 2026-06 strategic-relevance review (market + code sanity-check). Re-aims the solo build at the AI-telephony frontier — the reflex/tap/eval loop and the contact-center domain — and off the trunk-SIP rewrite.
- Amends:
README.md,docs/ARCHITECTURE.md,docs/PORT_PLAN.md - Related: ADR-0002 (fused vertical), ADR-0006 (ingress posture), ADR-0001 (the "rent the edge" instinct this re-converges with)
Context
ADR-0003 decided to own trunk SIP and media termination directly
in the Rust core (rsip + ezk), dropping the Kamailio + rtpengine shield so the memory-safety thesis
would be "literally true at the wire." The argument was internally coherent: under the contact-center
scope the device-interop tail collapses to a few documented carriers, so a bounded Rust trunk client
looked tractable.
A strategic-relevance review — the goal is staying at the AI-telephony frontier, not shipping a from-scratch protocol stack — stress-tested that bet against the 2026 market and the realities of a solo build. It does not survive:
- A trunk SIP/media core is the highest-cost, lowest-differentiation square on the board. End-to-end voice-agent latency is ~1,100 ms mouth-to-ear; the media/transport layer is ~80–100 ms of it, and PSTN transport is ~$0.02/min — a rounding error. The perceived-quality battle (turn-taking, barge-in, endpointing) and the buying criteria (integration, reliability, compliance, outcomes) all live above the transport. No buyer in this market has ever purchased on implementation language.
- There is no production-hardened pure-Rust SIP stack to stand on.
ezk-sipis the only credible one and it is single-maintainer / pre-1.0 (bus-factor-1);str0mself-describes as "not for production" for this purpose. Every serious player stands on mature C/Go and puts its own language only at the orchestration edge — LiveKit on Go/Pion, Daily wrapping libwebrtc, jambonz on FreeSWITCH + rtpengine. A solo founder re-fighting 25 years of carrier/NAT/DTMF hardening in a young Rust stack spends the one resource it cannot spare (years) on the layer that pays back least. - It steals those years from the actual white space. The differentiation versus LiveKit / Vapi / Pipecat is the contact-center domain (ACD, queues, escalation, CDR, recording, supervisor) plus the reflex loop plus the data-owned eval/containment loop — none of which a trunk-SIP rewrite gets you closer to.
ADR-0003's own sequencing already conceded the point: it put the reflex loop (spearhead steps 1–4) before the trunk (step 5) precisely because the reflexes are "the hard, most-differentiating part." Followed one step further, that logic says: don't make the trunk a first-party build at all.
Decision
rutster owns no SIP stack. Carrier/PSTN reach is rented transport, in three layers — and rutster owns only the top one:
- Primary (demo, frontier, most users) — rented CPaaS raw-media ingress. Zero SIP in rutster. A CPaaS raw-audio fork (Twilio Media Streams, Telnyx media streaming, or equivalent) delivers the PSTN call's audio (µ-law/PCM) over a WebSocket; call control (answer/originate/hangup) is the provider's REST/Call-Control API. This lands as a media-leg ingress — core-as-server, exactly parallel to the WebRTC ingress in ADR-0006 — resampled to the canonical tap format and fed straight into the reflex loop. Use the raw-audio fork, not managed Voice-AI products (Twilio ConversationRelay et al.) that perform STT/TTS for you and would consume the reflex loop that is rutster's differentiation.
- Graduation (sovereignty / on-prem customers) — out-of-tree SBC adapter. When PSTN media must stay on the operator's infra, a mature C/Go SBC (Kamailio / FreeSWITCH / drachtio + rtpengine) sits outside the trust boundary and B2BUAs carrier SIP into rutster as clean RTP / tap-format media. The operator operates C; rutster parses none. This is ADR-0006's out-of-tree edge-adapter pattern, applied to trunks.
- Never — a first-party Rust trunk SIP stack.
rsip/ezkstay off the critical path. If a thin Rust SIP UA is ever wanted, it is a discretionary, out-of-tree experiment — never a core dependency or a blocker.
The only things rutster owns in Rust are the call model, the reflex loop, and the agent tap. Everything that touches a carrier is rented (layer 1) or out-of-tree (layer 2).
Topology
carrier / PSTN rented or out-of-tree rutster trust boundary
──────────────► ┌──────────────────────────┐ ┌──────────────────────────────┐
SIP trunks, │ layer 1: CPaaS raw-media │ │ media (RTP/SRTP, str0m) │
far-end UAs │ fork — NO SIP in rutster│──►│ + reflexes (VAD/barge-in, │
│ provider API = control │ │ jitter, pacing) │
│ layer 2: out-of-tree SBC │ │ + in-boundary spend/abuse │
│ (C/Go) → clean RTP/tap │ │ + clean audio tap ──► brain │
└──────────────────────────┘ └──────────────────────────────┘
SIP lives here — outside rutster, never first-party no SIP parser, ever
WebRTC ingress (ADR-0006) is unchanged and remains the primary human-participant path; layer 1 adds a phone number without adding SIP.
What this costs (stated so we don't kid ourselves)
- Data-ownership / "terminate media once" dilutes for PSTN in layer 1. A CPaaS-forked PSTN call's audio traverses the provider's infra — the same property rutster criticizes in Vapi/Retell. What survives even in layer 1: WebRTC legs stay on your infra; the brain is self-hostable, so training data and model remain owned even when transport is rented; and layer 2 brings PSTN media back on-prem for buyers who require it. The wedge becomes a graduation, not a day-one guarantee — an honest narrowing the pitch must reflect (README competitor table + wedge bullet 2).
- A CPaaS dependency (billing, availability) for layer-1 PSTN. Mitigation: it is a config seam (provider-agnostic media-leg ingress), and the frontier work (reflexes, tap, brain) is built and demoed entirely on WebRTC, so nothing on the critical path blocks on any provider.
Memory-safety, restated honestly
ADR-0003's headline was "own a fuzzed Rust SIP parser → safe at the wire." ADR-0007's is stronger in
one respect and weaker in another, stated plainly: rutster parses no SIP at all. Its entire
first-party wire surface is WebRTC/RTP/SRTP (str0m) + the WebSocket tap/ingress protocol — all
memory-safe Rust, fuzzed. The carrier-SIP interop tail lives outside the trust boundary (the
provider's, or an out-of-tree SBC). The concession vs. 0003: layer 2 reintroduces C — but it is not
first-party, not on the per-call hot path inside the boundary, and is widely-deployed, battle-tested
infra the operator runs at the edge. rutster removes SIP from its own attack surface entirely rather
than rewriting it safely.
Consequences
- Positive: the solo-years redirect from a trunk-SIP rewrite to the white space (contact-center domain + reflex/eval loop + the tap-as-open-protocol); no first-party SIP attack surface; no bus-factor-1 dependency on a young Rust SIP stack; a real phone number becomes a small rented integration, not the riskiest step; first-call still never blocks on SIP.
- Negative: PSTN data-ownership becomes a layer-2 graduation rather than a day-one property (the pitch must be re-scoped honestly); a CPaaS dependency for layer-1 PSTN; the "memory-safe SIP parser" talking point is retired.
- Mitigation: provider-agnostic media-leg ingress at a config seam; the sovereignty wedge is preserved as the out-of-tree SBC path; the frontier is demoed end-to-end on WebRTC regardless of trunk choice.
Relationship to ADR-0001 and ADR-0003
This re-converges with ADR-0001's correct instinct — don't own the interop tail; rent the hardened edge — while keeping ADR-0003's correct instinct — don't front your own media plane with a double-terminating shield. The synthesis: the rented transport delivers clean media straight to the tap/ingress, so there is neither a first-party SIP tail (0001's worry) nor a second in-boundary media termination (0003's worry). 0001 rented an SBC in front of rutster's own media plane; 0007 rents transport that replaces the need for a first-party trunk entirely, with WebRTC + the tap as the media spine.