# Quickstart

Get Rutster running and hear your own voice echoed back in under 5 minutes.

> **Status:** Slices 1–4 (WebRTC media core, WS tap, OpenAI Realtime brain,
> barge-in / VAD-driven playout kill) are merged to `main`. Slice 4½
> (sim/benchmark harness) + step 5 (PSTN via rented Twilio Media Streams
> transport) are the active build targets, in flight. This quickstart's first
> section exercises the slice-1 WebRTC media loopback (the simplest end-to-end
> demo on `main`); the optional "Make a real phone call" section below covers
> step-5 PSTN ingress. See
> [`docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md`](superpowers/specs/2026-07-05-slice-5-rented-transport-design.md)
> for the active build target's design.

> **Fastest path:** if you want a *running deployment* — TLS, a domain, a phone
> number — skip the source build and use the Docker quickstart:
> [`docs/deploy/quickstart-docker.md`](deploy/quickstart-docker.md). This page
> is the from-source developer path.

---

## Prerequisites

### 1. Rust toolchain

Install via [rustup](https://rustup.rs/):

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

The repo pins a specific stable channel in `rust-toolchain.toml` — `rustup`
will pick it up automatically on first `cargo` invocation. No manual
toolchain selection needed.

### 2. libopus (FFI dependency)

The `opus` crate links system libopus via FFI (per PORT_PLAN §7's
"🦀 Core (FFI)" disposition — Opus is the codec surface Rust doesn't need
to re-implement). Install the dev headers:

| Platform | Command |
|---|---|
| Debian/Ubuntu | `sudo apt-get install -y libopus-dev` |
| Fedora | `sudo dnf install -y opus-devel` |
| Arch | `sudo pacman -S opus` |
| macOS (Homebrew) | `brew install opus` |

Verify: `pkg-config --cflags opus` should print a path with no error.

That's the only system dependency in slice 1. Everything else is pure
Rust from crates.io.

---

## Run the server

```bash
cargo run
# listening on http://0.0.0.0:8080
```

First build takes ~2 minutes (str0m + axum + tokio compile fresh).
Subsequent builds are incremental.

---

## Hear the echo

1. Open a browser to <http://localhost:8080/>.
2. Click **Start call**.
3. Grant microphone permission when the browser prompts.
4. Speak — you should hear yourself back within ~200 ms
   (no perceptible delay).
5. Click **Hang up** to tear down. The server logs
   `Closing → Closed` for the session.

Verbose tracing for debugging:

```bash
RUST_LOG=rutster=debug cargo run
```

---

## Make a real phone call (PSTN via Twilio Media Streams, optional)

The WebRTC loopback above proves the media core + the reflex loop. Spearhead
step 5 takes it to a **real phone number**: a PSTN caller dials your Twilio
number, Twilio forks the call's audio over a WebSocket to rutster, and the
same reflex loop (barge-in, brain tap) runs against the PSTN leg — **no
first-party SIP stack** ([ADR-0007](adr/0007-trunk-rented-transport.md)).

### Prerequisites

- A [Twilio account](https://www.twilio.com/) (trial works).
- A Twilio phone number capable of Voice + Media Streams.
- A publicly-reachable HTTPS URL for rutster (Twilio must call back to you).
  For local dev, use [ngrok](https://ngrok.com/) to tunnel `localhost:8080`
  to a public URL: `ngrok http 8080`.

### Twilio credentials

Set these environment variables to enable PSTN ingress:

| Env var | Purpose | Example |
|---|---|---|
| `RUTSTER_TWILIO_ACCOUNT_SID` | Twilio account ID | `ACxxx...` |
| `RUTSTER_TWILIO_AUTH_TOKEN` | Twilio auth token (secret; never logged) | `xxx...` |
| `RUTSTER_TWILIO_MEDIA_BIND` | Where rutster's Media Streams WSS server binds | `0.0.0.0:8081` |
| `RUTSTER_TWILIO_WEBHOOK_BASE` | Your public URL Twilio calls back to | `https://your.ngrok.io` |

Without these set, the binary runs WebRTC-only (slices 1–4 ingress + barge-in).
With them set, the binary accepts PSTN fork calls against `/twilio/media-stream`.

### Run with Twilio enabled

The live Twilio REST client is feature-gated behind `twilio-live` — the routine
CI gate uses the in-process mock; you only need the live client for a real call:

```bash
export RUTSTER_TWILIO_ACCOUNT_SID=AC...
export RUTSTER_TWILIO_AUTH_TOKEN=...
export RUTSTER_TWILIO_MEDIA_BIND=0.0.0.0:8081
export RUTSTER_TWILIO_WEBHOOK_BASE=https://your.ngrok.io
cargo run --features=twilio-live
```

### Point Twilio at rutster

1. In the Twilio console, configure your phone number's **A CALL COMES IN**
   webhook to `POST` to `https://your.ngrok.io/v1/trunk/webhook`.
2. Twilio answers an inbound call, hits your webhook; rutster responds with
   TwiML instructing Twilio to `<Connect><Stream>` against
   `wss://your.ngrok.io/twilio/media-stream`.
3. Twilio opens the WSS; rutster's `TwilioMediaStreamsServer` receives the
   call's audio (µ-law @ 8 kHz), decodes via `G711Codec` to 24 kHz PCM, and
   feeds the same reflex loop + brain tap as a WebRTC leg.

### Make an outbound call

```bash
curl -X POST http://localhost:8080/v1/trunk/sessions \
  -H 'Content-Type: application/json' \
  -d '{"to":"+15551234567","from":"+15550000000"}'
```

The handler calls `TwilioCallControlClient::originate`; Twilio places the call
and forks the audio back — the PSTN caller reaches the reflex loop.

> **Auth note:** the `/v1/trunk/*` routes are **not authenticated** in slice 5
> (authn defers to slice 6, the spend cap). Do not expose them to the public
> internet without a reverse-proxy auth layer.

### What's different from WebRTC

Nothing, architecturally. The PSTN leg enters the same `MediaThread` tick loop
via a `MediaLeg::Trunk(TrunkSession)` variant (the WebRTC leg is `MediaLeg::WebRTC`).
The reflex stack (`Reflex<TapAudioPipe>` + `LocalVadReflex`) is **reused
verbatim** from slice 4 — barge-in fires on PSTN caller speech the same way it
fires on WebRTC caller speech. The only difference is the ingress: str0m's RTP
decode (WebRTC) vs the Media Streams WSS pump + G.711 codec (trunk).

---

## Troubleshooting

| Symptom | Likely cause / fix |
|---|---|
| `error: linking with cc failed` / `could not find opus` | libopus dev headers not installed. Re-run the install command above. |
| Browser shows no mic prompt | Another tab/app holding the mic, or mic permissions disabled for `localhost`. Check browser settings. |
| `ICE connection failed` in the browser | Shouldn't happen on loopback (host candidates only). If it does, check the server console for the str0m error. |
| Click Start call, nothing happens | Open the browser console (F12). The page logs ICE state + connection state to a `<pre>` element. Look for the failure there. |
| Port 8080 already in use | Set `RUTSTER_HTTP_BIND`, e.g. `RUTSTER_HTTP_BIND=0.0.0.0:8090 cargo run -p rutster` |

The browser test page at `GET /` is a single self-contained HTML file
with inline JS — no build step. View source to see exactly what the
client side is doing.

---

## What's happening

When you click "Start call":

1. Browser captures microphone audio via `getUserMedia`.
2. Browser creates an `RTCPeerConnection` and generates an SDP offer
   (audio-only, Opus codec).
3. Browser POSTs the offer to `POST /v1/sessions/:id/offer`.
4. The Rutster core (built on [`str0m`](https://docs.rs/str0m), a sans-IO
   WebRTC implementation) accepts the offer, generates an SDP answer with
   its DTLS fingerprint + ICE credentials.
5. Browser sets the answer as remote description; ICE + DTLS handshake
   completes.
6. RTP starts flowing: browser → core terminates DTLS-SRTP → decodes
   Opus to 16-bit PCM @ 24 kHz mono → echoes PCM back → re-encodes to
   Opus → DTLS-SRTP → browser plays it.

The "codec-to-PCM boundary" is the canonical point where, in a future
slice, the audio tap for an external AI brain splices in. Slice 1 just
echoes; step 2 of the spearhead swaps the echo for a real tap.

For the why, see [`ARCHITECTURE.md`](ARCHITECTURE.md). For the dev loop,
see [`DEVELOPMENT.md`](DEVELOPMENT.md).
