docs(realtime): record the design decisions behind the realtime layer

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.
This commit is contained in:
2026-08-14 19:04:41 +02:00
parent 099315a538
commit 55a2af4756
14 changed files with 4470 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# Voice auf Linux
Type: grilling
Status: resolved
Blocked by: —
Parent: ../map.md
## Question
Was ist die Zusage an Linux-Nutzer bezüglich Sprachanrufen — und wenn Voice dort laufen
soll, über welchen Medien-Stack?
Aufgeworfen von [*Recherche: WebRTC im Tauri-WebView (Mobile)*](01-research-webrtc-tauri-mobile.md).
Die erwartete schlechte Nachricht kam nicht von Mobile, sondern von Linux.
### Befundlage (belegt, siehe §5 der Recherche)
WebRTC ist in Tauris Linux-WebView (WebKitGTK 4.1) an **drei unabhängigen Gates** blockiert:
1. **Compile-Time.** `ENABLE_WEB_RTC` folgt `ENABLE_EXPERIMENTAL_FEATURES`, das auf `OFF`
steht. Debian/Ubuntu und Alpine setzen es belegbar nicht → `RTCPeerConnection` existiert
im JS-Global gar nicht. Arch/Fedora unverifiziert (Experiment E5 in der Recherche).
2. **Runtime-Setting.** `WebKitSettings:enable-webrtc` ist Default `FALSE` und wird von wry
nie gesetzt.
3. **Permission.** `permission-request` ist Default Deny; wry 0.55.1 verbindet das Signal
überhaupt nicht.
Gate 2 und 3 wären aus dem App-Code über `WebviewWindow::with_webview` lösbar (bzw. ab
Tauri 2.12 sauber über `on_permission_request`). **Gate 1 hängt am Distro-Paket des
Nutzers und ist von uns nicht lösbar.**
Das trifft ausgerechnet die Gruppe, die für dieses Projekt zentral ist: Selbsthoster sind
linux-lastig.
### Optionen, alle unattraktiv
1. **Voice auf Linux als nicht unterstützt deklarieren.** Chat läuft voll, Voice nicht.
Ehrlich und billig — und wahrscheinlich der Weg, der die Simplizitätsdoktrin am wenigsten
verletzt. Aber schmerzhaft für die Kernzielgruppe.
2. **Nativer Medien-Stack nur auf Linux.** `livekit/rust-sdks` (`libwebrtc`-Crate, echtes
libwebrtc mit NetEQ/AEC/Opus, laut README inkl. Linux) im Tauri-Rust-Layer. Bricht die
Architektur-Einheitlichkeit: Voice läge auf drei Plattformen im WebView und auf einer in
Rust. Zusätzlich ein großer vorkompilierter nativer Blob pro Plattform.
*(`webrtc-rs` ist hierfür **kein** Ersatz — es ist ein reiner Protokoll-Stack ohne
Geräte-Capture, ohne Codecs, ohne Jitter-Buffer, ohne Echo-Cancellation.)*
3. **Nativer Stack auf allen Plattformen.** Einheitlich, aber verwirft den kostenlosen
WebView-Pfad auf drei von vier Zielen und zieht den nativen Blob überallhin.
4. **Eigenes WebKitGTK bündeln.** Widerspricht der Simplizitätsdoktrin frontal.
5. **Auf Upstream warten.** Löst Gate 2 und 3, **nie Gate 1**. Also keine Lösung.
### Zu entscheiden
- Welche Option — und wie wird die Einschränkung dem Nutzer gegenüber **sichtbar** gemacht,
statt ihn in einen Timeout laufen zu lassen? (Feature-Detection `typeof RTCPeerConnection`
ist trivial und sollte in jedem Fall passieren.)
- Falls (2) oder (3): wo verläuft dann die Rust↔WebView-Naht? Bei nativem Audio ist der
WebView **nicht mehr der Medien-Endpunkt** — Capture und Wiedergabe liegen in Rust, und
die UI bekommt nur heruntergerechnete Metering-Werte über IPC, niemals rohes PCM.
Zurück-Injizieren von PCM in den WebView geht in Chromium, aber **nicht** in WebKitGTK.
Rückkopplung auf [*Wo lebt die WS-Verbindung im Client*](05-wo-lebt-die-ws-verbindung.md).
**Beantwortet (14.08.2026): Linux ist eine vollwertige Client-Plattform.** Damit ist der
billige Ausweg verbaut — Option 1 bedeutet, auf einer erklärten Zielplattform ein
Kernfeature nicht anzubieten. Sie bleibt formal wählbar, muss aber als bewusster
Produktverzicht begründet werden, nicht als technische Notwendigkeit.
Realistisch bleiben damit **(2)** nativer Stack nur auf Linux und **(3)** nativer Stack
überall. Die eigentliche Frage dieses Tickets ist die Abwägung zwischen diesen beiden:
Einheitlichkeit gegen Kostenminimierung.
### Kopplung
Wirkt auf [*Voice-Topologie*](04-voice-topologie.md) zurück: fällt hier ein nativer Pfad,
ändern sich dort die Optionen. Die beiden Tickets können in beliebiger Reihenfolge laufen,
aber das zweite muss das Ergebnis des ersten kennen.
---
## Answer
Entschieden am 14.08.2026.
### 0. Zusätzlich geprüft: Flatpak löst Gate 1 nicht
Die Recherche hatte Gate 1 als „hängt am Distro-Paket" eingestuft. Das gilt aber nur, solange
der Nutzer das Distro-Paket benutzt — ein Flatpak bringt WebKitGTK aus seinem Runtime mit.
Geprüft, Ergebnis negativ:
- Das **GNOME-Runtime baut WebKitGTK ohne den WebRTC-Flag**; die Antwort im Flathub-Forum auf
die Frage nach dem Patchen lautete sinngemäß „macht ein Issue bei gnome-build-meta auf"
([Flathub Discourse](https://discourse.flathub.org/t/patching-gnome-runtime/10682)).
- Das einzige dokumentierte funktionierende Rezept verlangt, **WebKitGTK selbst zu
kompilieren** (`-DENABLE_MEDIA_STREAM=ON -DENABLE_WEB_RTC=ON`) **und zusätzlich wry zu
patchen** — erprobt auf NixOS unter X11, mit GBM-Fehlern unter Wayland und nicht
aufgelisteten v4l2loopback-Geräten
([Tauri Discussion #8426](https://github.com/tauri-apps/tauri/discussions/8426)).
Für ein Entwicklungssystem machbar, für Endnutzer nicht. **Gate 1 hat heute keine
nutzerseitige Lösung.**
### 1. WebView auf drei Plattformen, nativer Stack nur auf Linux
Windows, Android und iOS nutzen den kostenlosen, funktionierenden WebView-Pfad. Linux bekommt
`livekit/rust-sdks` (`libwebrtc`-Crate) im Rust-Layer — echtes libwebrtc mit NetEQ, AEC/AGC/NS
und Opus, laut README inklusive Linux.
`webrtc-rs` ist **kein** Ersatz: reiner Protokoll-Stack ohne Geräte-Capture, ohne Codecs, ohne
Jitter-Buffer, ohne Echo-Cancellation.
Die Alternative „nativ überall" war knapp und hat ein starkes Argument — ein Pfad statt zwei,
eine Berechtigungslogik, eine Testmatrix, und die wry-Eigenheit (Mikrofonrechte werden auf iOS
bedingungslos an jeden Origin vergeben) wäre komplett aus dem Spiel. Sie verliert, weil sie
einen auf drei Plattformen funktionierenden Gratis-Pfad wegwirft. **Der akzeptierte Preis sind
zwei Medienpfade auf Dauer**, mit zwei Berechtigungsmodellen und zwei Fehlerbildern.
**Nützliche Nebenwirkung:** weil Linux den nativen Stack *unbedingt* nutzt und nicht nur dort,
wo die WebView-Variante versagt, spielt es keine Rolle mehr, ob eine Distribution WebKitGTK
mit WebRTC gebaut hat. **Experiment E5 der Recherche (Arch/Fedora prüfen) entfällt damit.**
Und weil es hier nur um Audio geht, ist der GBM-/Wayland-Fehler aus #8426 gegenstandslos — er
betrifft die Videoausgabe des WebViews.
### 2. Die Naht ist eine explizite Verzweigung im Frontend, ganz oben
Rust meldet dem Frontend eine Fähigkeit — sinngemäß `mediaOwner: "webview" | "native" | "none"`
— und das Voice-Modul verzweigt **einmal**, an seiner Spitze. Darunter zwei klar getrennte
Implementierungen: `RTCPeerConnection` im WebView, oder Tauri-Commands in den nativen Stack.
Das ist ehrlich gegenüber der Realität, dass sich die Fälle nicht nur mechanisch
unterscheiden, sondern auch in der Oberfläche: **auf Linux gibt es kein `<audio>`-Element**,
weil PCM sich in WebKitGTK nicht zurückinjizieren lässt (in Chromium ginge es). Der native
Pfad besitzt dort die Wiedergabe vollständig, und Pegelanzeigen kommen als
heruntergerechnete Werte über IPC — **niemals rohes PCM**.
Verworfen: ein `MediaEngine`-Trait in Rust. Es wäre die sauberere Fassade, verlangte aber eine
Steuerungsumkehr — die WebView-Implementierung müsste ihre eigentliche Arbeit per IPC nach
oben ans Frontend delegieren. Ebenfalls verworfen: eine Naht nur auf Signaling-Ebene, weil die
Duplizierung dann bis in die Oberfläche reicht und Verhaltensunterschiede versehentlich statt
absichtlich entstehen.
### 3. Gestaffelt: Voice erscheint zuerst auf den WebView-Plattformen
Linux zeigt Voice solange sauber als „noch nicht verfügbar" an — über die gemeldete Fähigkeit
`none`, **nicht** über eine WebView-Feature-Erkennung, weil die auf Linux dauerhaft negativ
bliebe und nichts über den nativen Pfad aussagt. Chat läuft dort voll.
Das Argument ist Risikoreihenfolge: der WebView-Pfad validiert das Signaling-Protokoll billig
auf drei Plattformen, und der native Stack steckt danach in ein bereits bewährtes Protokoll.
Den schweren, unerprobten Medien-Stack gegen ein gleichzeitig unerprobtes Protokoll zu bauen,
verdoppelt die Unbekannten in einem Schritt — und bei einem Fehler wüsste niemand, welche
Seite schuld ist.
**Bewusst in Kauf genommen:** die Zielgruppe ist linuxlastig, und „Voice, aber nicht bei dir"
ist keine gute erste Begegnung. Wer es einmal als „geht auf Linux nicht" abgespeichert hat,
prüft später nicht nach. Das ist der Preis dafür, das Feature überhaupt in Nutzerhände zu
bringen, bevor Monate in den nativen Stack fließen — und er ist erträglich, weil die Ansage
ehrlich und sichtbar ist statt stillschweigend.
### Folgen für andere Tickets
- [*Voice-Topologie*](04-voice-topologie.md): der native Stack liefert auf Linux echtes
libwebrtc, Mesh funktioniert dort also genauso. Die Topologie-Entscheidung wird durch dieses
Ticket **nicht** eingeschränkt.
- [*Voice-Signaling-Protokoll*](09-voice-signaling-protokoll.md): der signierte
DTLS-Fingerprint muss auf **beiden** Pfaden erzeugbar sein — im WebView über
`RTCPeerConnection.generateCertificate()`, nativ über die Zertifikats-API von libwebrtc. Die
Naht für das Pinning darf also nicht an einer WebView-API hängen.