# ADR-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack

- **Status:** Accepted
- **Date:** 2026-06
- **Supersedes:** [ADR-0003](0003-sip-rust-native-trunk.md) — 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](0002-north-star-and-fused-core.md) (fused vertical), [ADR-0006](0006-ingress-posture.md)
  (ingress posture), [ADR-0001](0001-sip-strategy.md) (the "rent the edge" instinct this re-converges with)

## Context

[ADR-0003](0003-sip-rust-native-trunk.md) 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:

1. **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.
2. **There is no production-hardened pure-Rust SIP stack to stand on.** `ezk-sip` is the only credible
   one and it is single-maintainer / pre-1.0 (bus-factor-1); `str0m` self-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.
3. **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:

1. **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](0006-ingress-posture.md) — 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.
2. **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](0006-ingress-posture.md)'s out-of-tree edge-adapter pattern, applied to trunks.
3. **Never — a first-party Rust trunk SIP stack.** `rsip` / `ezk` stay 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](0006-ingress-posture.md)) 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](0001-sip-strategy.md)'s correct instinct — *don't own the interop
tail; rent the hardened edge* — while keeping [ADR-0003](0003-sip-rust-native-trunk.md)'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.

## References

- [ADR-0003](0003-sip-rust-native-trunk.md) — superseded (Rust-native trunk SIP)
- [ADR-0001](0001-sip-strategy.md) — the original "rent the edge" instinct this re-converges with
- [ADR-0002](0002-north-star-and-fused-core.md) — fused per-call vertical (the boundary this re-scopes)
- [ADR-0006](0006-ingress-posture.md) — WebRTC-first ingress; out-of-tree edge-adapter pattern (applied here to trunks)
