Eleven resolved decision tickets plus two research notes, produced with /wayfinder before any code was written. They live next to the code they explain rather than in the issue tracker: they are reasoning documents, not open work, and in a tracker they would drift from the code with no diff and no review. The value is in the rejected alternatives. Why a counter under a row lock and not BIGSERIAL, why an event log and not a column on messages, why resync is a 200 with a discriminated body and not a 4xx, why the fan-out trait was deleted again, why voice is peer-to-peer with a hard six-person cap and no speculative SFU seam, and why Linux needs a native media stack while the other three platforms do not. Two entries record where the spec was wrong and got corrected: created_at was never a safe sort key, and an unread count cannot be derived without fetching because seq counts events rather than messages. Open work is tracked as issues, not here.
8.2 KiB
Voice-Topologie: Mesh oder SFU
Type: grilling Status: resolved Blocked by: — Parent: ../map.md
Question
Läuft Audio direkt zwischen den Clients (P2P-Mesh) oder über einen serverseitigen Medien-Router (SFU) — und was davon legt das Signaling-Protokoll heute schon fest?
Diese Frage wurde beim Charting bewusst offengelassen (Q6). Entblockt am 14.08.2026: Recherche: WebRTC im Tauri-WebView (Mobile) hat bestätigt, dass Android, iOS und Windows WebRTC im WebView tragen. Linux nicht — das ist eine eigene Frage in Voice auf Linux, deren Antwort auf diese hier zurückwirken kann.
Spannungsfeld
- Mesh passt zum Bedrohungsmodell: der Server sieht nie Klartext-Audio, DTLS-SRTP bleibt zwischen den Peers. Passt außerdem zur harten Deployment-Regel — nichts Zusätzliches zu betreiben. Skaliert aber nicht: bei n Teilnehmern hält jeder n−1 Verbindungen und lädt sein Audio n−1 mal hoch. Praktische Obergrenze liegt bei etwa vier bis sechs Teilnehmern, auf Mobile eher darunter.
- SFU skaliert auf Dutzende Teilnehmer, terminiert aber DTLS und sieht damit den Audio-Klartext. Das widerspricht dem Bedrohungsmodell „Betreiber ist Angreifer" frontal. E2EE über einen SFU braucht Insertable Streams / SFrame — das ist ein eigenes Forschungsprojekt und im Rust-Ökosystem dünn besiedelt. Zusätzlich ein weiterer zu betreibender Dienst, gegen die Simplizitätsregel.
Zu entscheiden
- Welche Teilnehmerzahl muss ein Voice-Room realistisch tragen? Zwei Personen im Gespräch, ein Team-Standup mit acht, oder ein Discord-artiger Kanal mit dreißig? Diese Zahl entscheidet die Frage fast allein.
- Wenn Mesh: was passiert oberhalb der Grenze — harte Begrenzung mit klarer Fehlermeldung, oder Degradation?
- Wenn SFU irgendwann: welche Nähte muss das Signaling-Protokoll heute schon haben, damit der Wechsel kein Bruch ist? (Wer bietet an, wer antwortet, wie werden Streams identifiziert.)
- TURN: P2P scheitert in restriktiven Firmennetzen ohne Relay — und Firmen sind Zielgruppe. Bestätigt ist bereits: TURN bleibt optionale externe Abhängigkeit. Was genau ist die Zusage an den Nutzer, wenn kein TURN konfiguriert ist? Und wie merkt der Client das früh genug, um es ehrlich anzuzeigen statt in einen Timeout zu laufen?
- Wie werden TURN-Credentials übermittelt, ohne sie fest in den Client zu backen? (Zeitbeschränkte HMAC-Credentials sind der übliche Weg — gehört das ins Signaling-Protokoll?)
Zwei SDP-Fragen aus der WebRTC-Recherche, die hierher gehören
Beide sind formal Codec-/Transport-Themen, aber es sind SDP- bzw. ICE-Entscheidungen und damit Topologie-nah:
iceTransportPolicy: "relay"erzwingen? Verhindert, dass der Peer die IP-Adresse des Gegenübers lernt (W3C WebRTC 1.0 nennt das ausdrücklich als Zweck). Der Server kennt die IP ohnehin aus der WebSocket-Verbindung, also kostet es nichts an Vertraulichkeit gegenüber dem Betreiber — es reduziert strikt die Exposition gegenüber dem Peer. Preis: ohne TURN gibt es dann gar keine Verbindung mehr, auch im LAN nicht.- Opus mit
cbr=1(und ggf.usedtx=0) verhandeln? Variable Bitrate leckt über Paketgrößen ein Sprech-/Pausen-Muster — ein feindlicher TURN-Betreiber bekommt damit eine Voice-Activity-Detection frei Haus, obwohl er den Klartext nicht sieht. Konstante Bitrate schließt das gegen Bandbreite.
Randbedingung
Diese Entscheidung darf die Grenze „dieser Effort endet beim Signaling" nicht überschreiten. Codecs, Echo-Cancellation und Audio-Pipeline sind out of scope — die beiden Punkte oben sind die Ausnahme, weil sie im SDP ausgehandelt werden und damit ins Protokoll gehören.
Answer
Entschieden am 14.08.2026.
1. Mesh mit harter Obergrenze von sechs Teilnehmern
P2P-Mesh, DTLS-SRTP bleibt zwischen den Peers, der Server fasst nie Audio an. Das entspricht dem Bedrohungsmodell direkt und braucht keinen weiteren Dienst — die harte Regel „eine Binary + Postgres" bleibt für Voice unangetastet.
Oberhalb von sechs wird der Beitritt abgelehnt, nicht degradiert: eine klare Meldung ist ehrlicher als ein Gespräch, das für alle schlechter wird, ohne dass jemand versteht warum. Bei sechs Teilnehmern lädt jeder Client fünf Ströme hoch, also grob 120 kbit/s aufwärts — die Grenze, ab der Mesh in der Praxis kippt.
Kein spekulativer Vorbau für einen späteren SFU. Das ist der Kern der Entscheidung und bewusst gegen den Reflex getroffen: ein SFU-Protokoll ist client-zu-Server adressiert, ein Mesh-Protokoll peer-zu-peer. Eine „SFU-taugliche Naht" heute wäre Selbsttäuschung — sie würde Komplexität kosten und im Ernstfall trotzdem nicht passen. Ein SFU wäre ein bewusster Protokollbruch in einem eigenen Effort, und das steht so in der Map.
Verworfen: SFU von Anfang an. Er trüge Dutzende Teilnehmer, und SFrame/Insertable Streams
sind laut Recherche auf allen Zielplattformen verfügbar (WKWebView ab 15.4, Chromium ab 141).
Dagegen stehen drei Kosten: ein weiterer zu betreibender Dienst gegen die Deployment-Regel,
ein erheblich schwereres Krypto-Design zulasten des E2EE-Efforts, und selbst mit SFrame
behielte der SFU Zugriff auf RTP-Metadaten — die audio-level-Header-Erweiterung ist ein
direktes Sprechaktivitäts-Orakel und müsste eigens wegverhandelt werden.
Ebenfalls verworfen: den SFU dauerhaft auszuschließen. Das hätte das Profil geschärft, aber die Tür für Firmenkunden mit größeren Runden zugezogen — eine Produktentscheidung, die es ohne Not heute nicht braucht.
2. Eine Datenschutz-Politik je Instanz, vom Server gesetzt
Die Instanz konfiguriert einen Schalter, der zwei Dinge zugleich regelt und im Signaling an den Client übermittelt wird; der Client zeigt sichtbar an, welcher Modus gilt.
| Schalterstellung | iceTransportPolicy |
Opus |
|---|---|---|
| streng | "relay" erzwungen — der Peer lernt die IP-Adresse nie |
cbr=1, usedtx=0 |
| offen | Standard-ICE (host / srflx / relay) | Standard (VBR mit DTX) |
Die Kopplung ist nicht Bequemlichkeit, sondern folgt der Bedrohung: beide Lecks entstehen am selben Ort. Läuft Audio über das Relay, sieht der Betreiber — der erklärte Angreifer — die Paketgrößen und hat damit einen Sprechaktivitäts-Detektor, obwohl er den Inhalt nicht lesen kann. Läuft es direkt, bekommt er die Pakete gar nicht zu sehen, dafür lernt der Peer die IP. Ein Schalter, ein anzuzeigender Zustand.
Warum je Instanz und nicht global: auf einem öffentlichen Server telefonieren Fremde miteinander, und dass zwei Unbekannte gegenseitig ihre Anschlussadresse lernen, ist für ein Datenschutzprodukt untragbar — dort gehört „streng" hin, und TURN wird auf dieser Instanz zur Voraussetzung. Auf einer Firmeninstanz sind die Peers Kollegen im selben Netz; dort wäre erzwungenes Relay nur Bandbreitenverschwendung, und ein Gespräch zwischen zwei Rechnern im selben Büro liefe ohne Not über den Server.
Bewusst in Kauf genommen: im offenen Modus bleibt das Sprechmuster für Netzbeobachter auf dem Pfad sichtbar, und die Zusage an den Nutzer hängt am Betreiber — deshalb muss der Client sie anzeigen und nicht nur intern kennen.
Damit bleibt TURN die optionale Abhängigkeit, die die Map festgelegt hat: ohne TURN läuft Voice im offenen Modus eingeschränkt (Firmen-NATs), im strengen Modus gar nicht — und der Betreiber weiß, worauf er sich einlässt, weil er den Schalter selbst umlegt.
3. Beide Medienpfade müssen die Politik umsetzen
Aus Voice auf Linux: RTCPeerConnection im WebView auf drei
Plattformen, livekit/rust-sdks auf Linux. Sowohl iceTransportPolicy als auch die
Opus-fmtp-Parameter müssen auf beiden Wegen gesetzt werden können — beides ist
Standard-WebRTC und in beiden Stacks vorhanden, gehört aber in die Spec, damit es nicht auf
einem der Pfade vergessen wird.
Folgen
Voice-Signaling-Protokoll ist damit entblockt. Zwei Dinge gehen von hier dorthin: die Raumgröße muss serverseitig durchgesetzt werden (der Beitritt wird abgelehnt, nicht nur in der UI verhindert), und die Politik-Übermittlung gehört ins Signaling — zusammen mit den TURN-Zugangsdaten, die im strengen Modus zwingend sind.