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.
24 KiB
Map: WebSocket-Realtime-Layer + Voice-Signaling-Fundament
Status: abgeschlossen. Alle elf Tickets sind aufgelöst; das Ziel — eine implementierbare Spec — ist erreicht. Dieses Verzeichnis ist die Begründungsschicht zum Realtime-Code: warum etwas so gebaut ist, samt der verworfenen Alternativen. Offene Arbeit steht nicht hier, sondern als Issue im GitLab.
Der Effort umfasst beide Repos (rustification-server und rustification-client);
die Dokumente liegen hier, weil das Gewicht der Spec serverseitig ist.
Entstanden mit /wayfinder; Label dort: wayfinder:map.
Destination
Eine implementierbare Spec für einen WebSocket-Realtime-Layer neben dem bestehenden REST-API (Nachrichten-Push für alles Persistierte) plus das Voice-Signaling-Fundament (Room join/leave, Teilnehmerliste, SDP/ICE-Relay — kein Audio), zugeschnitten für den Tauri-Client auf Desktop und Mobile im Foreground.
Fertig, wenn jemand die Spec ohne Rückfragen implementieren kann und der spätere E2EE-Effort sie nicht ungültig macht.
Dieser Effort plant, er baut nicht. Jedes Ticket beantwortet eine Entscheidung. Die Umsetzung ist ein eigener Effort.
Notes
Produktkontext
Rustification ist als Alternative zu Matrix gedacht, mit zwei erklärten Erfolgsfaktoren:
- Deployment-Simplizität — wie leicht, schnell und reproduzierbar ist ein Server aufgesetzt und einsatzbereit?
- Nutzersicherheit — Schutz vor Angriffen und Data-Leaks.
Zielbild: Firmen, die mit sensiblen Daten umgehen (eigene Instanz), und eine öffentliche Discord-Alternative für Privatnutzer. Jeder Nutzer soll einen eigenen Server hosten können.
Bedrohungsmodell: der Serverbetreiber ist ein Angreifer. Das ist der Grund, warum echtes E2EE gerechtfertigt ist und nicht bloß Verschlüsselung at rest.
Codebases
rustification-server |
Rust, Axum 0.8, SQLx 0.9 / PostgreSQL 17, JWT + Argon2, Tokio. Migrationen embedded. |
rustification-client |
Tauri v2. Frontend React 18 + TS (Vite). Client-Backend Rust, ruft REST via reqwest. Ziele: Windows, Linux, Android, iOS. |
Stand heute: Polling. MessageList.tsx:33 fährt ein setInterval(load, 3000),
zusätzlich zählt ChatPage.tsx nach dem Senden einen refreshKey hoch. Beides wird
ersetzt. (Der Client-README beschreibt darüber hinaus Member-Panel und Settings-Seite,
die es im Code noch nicht gibt.)
Weitere Bestandsfakten aus den Recherchen: axum = "0.8.9" ist ohne ws-Feature
eingebunden, es gibt keinen Graceful Shutdown, und Extension<Claims> wird nirgends gelesen.
Auf Client-Seite existieren src-tauri/gen/android und gen/apple noch nicht — Mobile ist
nie initialisiert worden, es gibt also nichts zu migrieren. Und es gibt weder WebRTC- noch
WebSocket-Code: grüne Wiese.
Feststehende Randbedingungen
Diese sind entschieden und werden von den Tickets nicht neu aufgemacht:
| Thema | Entscheidung |
|---|---|
| REST vs. WS | REST bleibt für Auth, CRUD und History. WS macht Push + Voice-Signaling. |
| Protokollform | Dumb Pipe {conversation, cursor} für alles Persistierte; eigenes flüchtiges Format nur für Voice-Signaling. Die Naht verläuft entlang persistiert vs. flüchtig. |
| Deployment | Harte Regel: eine Binary + Postgres. TURN ist optionale, extern konfigurierbare Abhängigkeit — ohne sie läuft Chat voll, Voice eingeschränkt. |
| Zustellung | Best-effort + ?since-Backfill nach Reconnect. Keine Server-Queue, keine ACKs — die DB ist die Queue. |
| Fan-out | Revidiert am 14.08.2026 durch Fan-out-Architektur: kein Trait, keine zweite Implementierung — immer über pg_notify, auch bei einer Instanz. Ein Trigger auf conversation_events feuert, der Server hört sich per PgListener selbst zu. Ein Pfad für Ein- und Mehr-Instanz-Betrieb, der getestete Pfad ist der Produktionspfad. Kein Redis. (Ursprünglich: In-Process hinter einem Trait, NOTIFY als zweite Impl — formuliert, bevor feststand, dass der Event 60 Byte groß ist und der Trigger ohnehin feuert.) |
| Server-Modell | Keine Föderation. Der Client hält N Verbindungen zu N unabhängigen Servern; kein Server-zu-Server-Protokoll. |
| Krypto | Nachrichteninhalte sind im Protokoll undurchsichtige Blobs. Metadaten darf der Server sehen. Volles E2EE-Design ist ein Folge-Effort. |
| Mobile | Foreground ja. Background-Delivery und Push out of scope. |
| Bestandsdaten | Keine. Migrationen dürfen brechen. |
| Client-Plattformen | Windows, Linux, Android, iOS — Linux ist vollwertige Client-Plattform, nicht nur Server-Host (bestätigt 14.08.2026). Das verbaut den billigen Ausweg bei Voice auf Linux. |
| Keine fremden Origins im WebView | wry vergibt Mikrofonrechte ohne App-Beteiligung — auf iOS in 0.55.1 bedingungslos Grant für jeden Origin. Solange nur eigener gebündelter Code läuft, ist das harmlos; ein <iframe>, eine Link-Vorschau oder eine Einbettung fremder Inhalte bekäme sofort Mikrofonzugriff, ohne dass die App gefragt wird. Ab Tauri 2.12 (on_permission_request) kontrollierbar — bis dahin ist diese Regel der einzige Schutz. |
Skills pro Session
Jede Session, die ein Ticket bearbeitet: /grilling und /domain-modeling.
Recherche-Tickets: /research als Subagent. Prototyp-Tickets: /prototype.
Council-Verdikt (14.08.2026)
Position: Zuschnitt grundsätzlich richtig, aber zu groß und an einer Stelle sachlich falsch — Protokoll auf einen generischen Änderungs-Event reduzieren, Mobile-WebRTC- Recherche vor alle Voice-Entscheidungen ziehen. Konfidenz 71 %.
Drei kritische Risiken:
- Cursor-Lücke — aufgelöst am 14.08.2026: Zähler pro Conversation unter Zeilen-Lock, damit ist Commit-Reihenfolge gleich Cursor-Reihenfolge. Siehe Monotoner Cursor.
- Mobile-WebRTC-Unbekannte — aufgelöst am 14.08.2026, positiv: Android, iOS und Windows tragen WebRTC im WebView. An seine Stelle tritt ein neues Risiko, das die Recherche gefunden hat: Linux trägt es nicht, und Linux ist die Heimplattform der Zielgruppe Selbsthoster. → Voice auf Linux.
- Bedrohungsmodell-Lücke — aufgelöst: der Betreiber ist Angreifer (siehe oben).
Korrektur am Verdikt: das Argument „Dumb Pipe ist E2EE-agnostisch, weil kein Inhalt
durch den Socket geht" ist falsch — unter E2EE ist der Inhalt ohnehin ein Blob, den
der Server nicht lesen kann. Der Dumb Pipe gewinnt bei Simplizität, NOTIFY-Tauglichkeit
und Cache-Konsistenz, nicht bei der Sicherheit.
Minderheitsvotum (🎨 Visionary): „Matrix ist seit zehn Jahren exakt dieses Produkt — ein Matrix-Client in Tauri bringt dich in einem Wochenende weiter." Bewusst verworfen: Föderation ist der Grund für Matrix' Betriebsaufwand, und Simplizität im Deployment ist hier das Differenzierungsmerkmal.
Decisions so far
- Recherche: WebRTC im Tauri-WebView (Mobile) —
iOS, Android und Windows tragen WebRTC im WebView (iOS ab 14.3 mit
NSMicrophoneUsageDescription; Android ab API 21 mitRECORD_AUDIOundMODIFY_AUDIO_SETTINGS; Windows ohne Konfiguration). Linux nicht — drei unabhängige Gates in WebKitGTK, eines davon distro-seitig unlösbar. Council-Risiko 2 positiv aufgelöst. Zwei Befunde mit Folgen: der Signaling-Server kann DTLS-Fingerprints tauschen (RFC 8827) und der spezifizierte Gegenmechanismus ist nirgends implementiert; wry vergibt Mikrofonrechte ohne App-Beteiligung. Details:research/01-webrtc-tauri-mobile.md - Recherche: WS-Auth, Heartbeat und Reconnect —
Faktenlage zu allen sechs Teilfragen, ohne Empfehlung. Vier Befunde mit Folgen: (a) der
Authorization-Header ist der einzige Kanal, der in den Proxy-Defaults nicht mitgeloggt wird (Caddy redigiert ihn aktiv,request.urinicht) — undSec-WebSocket-Protocolwird sehr wohl geloggt; (b) die Browser-API muss laut WHATWG alle Fehlerdetails verschlucken, im WebView ist „Token abgelaufen" nicht von „Server down" unterscheidbar, im Rust-Layer schon; (c)axum::serve(...).with_graceful_shutdown(...)beendet WS-Verbindungen nicht und wartet nicht auf sie — eigenes Tracking ist zwingend; (d)tokio::sync::watchpasst semantisch besser zum Dumb Pipe alsbroadcast. Details:research/03-ws-auth-reconnect.md - Monotoner Cursor — ein Zähler
seqpro Conversation, vergeben perUPDATE conversations SET seq = seq + 1 … RETURNINGin derselben Transaktion wie die Änderung. Der Zeilen-Lock erzwingt Commit-Reihenfolge ==seq-Reihenfolge; die Korrektheit folgt aus der Konstruktion. Die Änderungsachse ist eine neue Tabelleconversation_events(die Log-Zeile ist der Dumb-Pipe-Event), nicht eine Spalte aufmessages— nur so werden Edits, Soft-Deletes und Reactions vom Backfill erfasst.?since=Nliefert aufgelösten Zustand inkl. Tombstones, nicht rohe Events: idempotent und koaleszierend, und das Log bleibt reine Signalisierung. Keine Retention, dafür ein Antwortlimit mitresync-Antwort — derselbe Pfad bedient auch Erstverbindung undRecvError::Lagged. Nebenbefund:created_atverliert seine Sortierrolle — es istNOW(), also Transaktionsstart, und die heutige Sortierung inlist_messagesist damit schon jetzt falsch. Council-Risiko 1 aufgelöst. - Wo lebt die WS-Verbindung im Client — eigene
Implementierung im Tauri-Rust-Layer (
tokio-tungstenite), Pool imAppState, Zustellung peremit. Entscheidend warenAuthorization: Bearer(einziger Kanal, den die Proxy-Defaults nicht loggen) und dasstungstenite::Error::Httpdie volle Antwort trägt, während der WebView laut WHATWG nur1006sehen darf.tauri-plugin-websocketgeprüft und verworfen (dünne API, dokumentierter Nachrichtenverlust, von den Maintainern erklärtermaßen nicht gepflegt). Voice-Signaling läuft ebenfalls über diesen Socket, nur dieRTCPeerConnectionlebt im WebView — die im Ticket vermutete Spannung löst sich auf. Rust macht den Backfill selbst und emittiert aufgelöste Entitäten, die Cursor-Buchhaltung liegt also bei Verbindung und Token statt im Frontend. Alle konfigurierten Server sind im Vordergrund verbunden.BASE_URLwird zur Server-Liste imtauri-plugin-store; Geheimnisse kommen hinter einSecretStore-Trait, dessen Implementierung der E2EE-Effort wählt. - Dumb-Pipe-Event & Backfill-Endpunkt — Event ist
minimal
{conversation, seq}(~60 Byte, JSON, keine Protokollversion);kindundtarget_idbewusst weggelassen, weil additiv nachrüstbar.conversation_events.kindbleibt damit serverintern. Aufholen nach Reconnect läuft über ein Manifest (GET /syncliefert{conversation_id, seq}für alle Mitgliedschaften — eine Abfrage) plus gezielte Backfills: bei 40 Conversations und 3 geänderten 1+3 Requests statt 40.resyncist ein unterschiedener Antworttyp bei 200, kein 4xx — es ist der Normalfall bei jeder Erstverbindung und darf nicht durch Fehlerpfade laufen. Limit 500 Entitäten, Tombstones werden mitgeliefert. Klartext-Grenze konkret gezogen und damit bindend für den E2EE-Effort: Blob werdenmessages.content,attachments.filename,attachments.mime_typeundreactions.emoji(deterministisch, damit derUNIQUE-Constraint greift); Routing-, Ordnungs- und Autorisierungsfelder plus Zeitstempel bleiben lesbar. Drei Endpunkte:/sync,/conversations/{id}/changes,/conversations/{id}/messages(seitenweise). - Fan-out-Architektur — Registry
conversation_id → Verbindungenplus eine zweiteuser_id → Verbindungenfür Mitgliedschaftswechsel (ein Beitretender steht noch nicht in der ersten). Je Verbindung ein Dirty-SetHashMap<Conversation, seq>+Notifystatt eines Kanals — perfektes Zusammenfassen, kein Überlauf, kein Überlauf-Fehlerpfad; Eintrag alsmax(), weilNOTIFYReihenfolge nur je Listener garantiert. Log-Zeile schreibt der Handler,pg_notifyfeuert ein dreizeiliger Trigger aufconversation_events— damit ist der Signalpfad unvergesslich. Der Trait ist gestrichen (siehe Randbedingung Fan-out oben). Zwei akzeptierte Kosten:PgListenerbelegt eine dedizierte Verbindung außerhalb des Pools, und eine volle Notify-Queue lässt Transaktionen beim Commit scheitern. Eine eingeführte Lücke ist geschlossen: nach jedem Listener-Reconnect geht ein Resync-Hinweis an alle Verbindungen, weilNOTIFYkeine Historie hat. Tragende Einsicht des Tickets: weil nur der neuesteseqje Conversation zählt, darf das Fan-out zusammenfassen und sogar verlieren — Lag ist Latenz, nicht Datenverlust. - Verbindungsaufbau & Auth über WS —
Authorization: Bearerim Handshake. In-Band-Re-Auth statt Verbindungs-TTL: der Client schiebt das erneuerte Token über den ohnehin bidirektionalen Socket, statt bei 15 Minuten Tokenlebensdauer zwölfmal pro Stunde neu zu verbinden; Widerruf wirkt trotzdem innerhalb der Restlaufzeit. Autorisierung liegt bei den REST-Endpunkten, nicht bei der Registry — der Event trägt keinen Inhalt, Registry-Drift leckt also höchstens Metadaten. Client pingt alle 15 s (Protokoll-Ping, in Rust verfügbar; 15 s wegen des striktesten belegten Proxy-Idle-Timeouts von 20 s), Server führtlast_seen. Acht Verbindungen je Nutzer, die älteste weicht — nicht die neue abweisen, sonst sperrt ein hängengebliebener Socket das Gerät aus. Eigene Close-Codes, drei Handlungsklassen (sofort / Backoff / gar nicht), Full Jitter beim Backoff. Graceful Shutdown wird selbst gebaut (CancellationToken+TaskTracker), weil axum WS-Verbindungen nicht abwartet. - Voice auf Linux — Flatpak zusätzlich geprüft und als Lösung
ausgeschlossen: das GNOME-Runtime baut WebKitGTK ohne WebRTC-Flag, und das einzige
funktionierende Rezept verlangt, WebKitGTK selbst zu kompilieren und wry zu patchen.
Gate 1 hat keine nutzerseitige Lösung. Entscheidung: WebView-Pfad auf Windows, Android und
iOS; nativer Stack (
livekit/rust-sdks) nur auf Linux — der Gratis-Pfad auf drei Plattformen wiegt schwerer als die Einheitlichkeit; akzeptierter Preis sind zwei Medienpfade auf Dauer. Die Naht ist eine explizite Verzweigung im Frontend, gesteuert über eine von Rust gemeldete Fähigkeit (webview/native/none), kein Trait mit Steuerungsumkehr. Gestaffelt: Voice erscheint zuerst auf den drei WebView-Plattformen, Linux zeigt es ehrlich als noch nicht verfügbar — der WebView-Pfad validiert das Signaling-Protokoll billig, bevor der schwere native Stack hineinsteckt. Nebenwirkung: Experiment E5 der WebRTC-Recherche (Arch/Fedora prüfen) entfällt, weil Linux den nativen Stack unbedingt nutzt. - Voice-Topologie — Mesh, harte Obergrenze sechs
Teilnehmer, serverseitig durchgesetzt; der Server fasst nie Audio an. Kein spekulativer
SFU-Vorbau: ein SFU-Protokoll ist client-zu-Server adressiert, ein Mesh-Protokoll
peer-zu-peer — eine „SFU-taugliche Naht" heute wäre Selbsttäuschung. Ein SFU wäre ein
bewusster Protokollbruch in einem eigenen Effort, nicht ausgeschlossen, aber auch nicht
vorbereitet. Dazu eine Datenschutz-Politik je Instanz, vom Server gesetzt und im
Signaling übermittelt: streng =
iceTransportPolicy: "relay"plus Opuscbr=1/usedtx=0, offen = Standard-ICE und Standard-Opus. Die Kopplung folgt der Bedrohung — beide Lecks entstehen am selben Ort: über das Relay sieht der Betreiber Paketgrößen und damit Sprechaktivität, direkt sieht er nichts, dafür lernt der Peer die IP. Der Client zeigt den Modus an. TURN bleibt damit optional: ohne TURN läuft Voice im offenen Modus eingeschränkt, im strengen gar nicht — und der Betreiber legt den Schalter selbst um. - Voice-Signaling-Protokoll — Discord-Modell:
voice_roomsist eine eigenständige Entität mit zwei möglichen Eltern —workspace_id(dauerhafter Sprachkanal, für alle Workspace-Mitglieder sichtbar und betretbar) oderconversation_id(Direktanruf aus DM/Gruppe, endet beim Auflegen), perCHECKgenau eines. Das spiegelt das Muster, dasconversationsschon benutzt. Autorisierung läuft durch genau eine Funktion, weil Rollen später Conversations und Räume gleichermaßen einschränken sollen. Raumzustand invoice_participantsin Postgres — nötig, weil Mehr-Instanz-Betrieb seit Ticket 08 ab Tag eins möglich ist; Geisterzeilen fängtlast_seenüber den bestehenden 15-s-Heartbeat ab. Adressierung übersession_id, nichtuser_id(Multi-Device). Der Beitretende bietet an — Konvention, kein Mechanismus; bei Video/Screenshare später durch Perfect Negotiation zu ersetzen. Fingerprint-Pinning: Naht jetzt, Füllung durch den E2EE-Effort — bis dahin zeigt der Client dauerhaft und nicht wegklickbar an, dass das Gespräch unverifiziert ist. Eine eigene Voice-Identität wurde bewusst verworfen, weil Nutzer sonst später zwei Identitäten und zwei Verifikationsrituale hätten. Politik und zeitbeschränkte TURN-Credentials reisen in der Beitrittsantwort; Raumgröße wird serverseitig durchgesetzt. - Client-Integration (Prototyp:
rustification-client, Branchprototype/frontend-zustandsmodell) — Der Client vergibt die Nachrichten-ID, nicht der Server. Der Prototyp hat gezeigt, dass optimistisches Senden sonst nicht zusammenführbar ist: dieselbe Nachricht steht zweimal da, einmal mitseqund einmal ohne.messages.idkommt damit vom Client statt ausDEFAULT gen_random_uuid()— Nebengewinn: Senden wird idempotent, ein Wiederholungsversuch ist ein No-op. Zweiter Befund: eine Ungelesen-Zahl ist ohne Nachladen nicht ableitbar, weilseqEvents zählt und nicht Nachrichten. Daraus die Regel: Rust lädt für DMs nach (Zahl), für Channels und Gruppen nicht (nur ein Punkt) — dasselbe Discord-Verhalten wie beim Raummodell, undconversations.kindgibt die Unterscheidung schon her. Zustandsform: je Server ein Verbindungszustand, je Conversation{geladen, ungelesen, nachrichten[]}; keine Zustandsbibliothek nötig, weil es keinen abgeleiteten Zustand gibt.
Not yet specified
- Presence, Typing-Indikator und Read-Receipts als Event-Typen. Bewusst hinter das
erste lauffähige Push geschoben — es sind Goodies, nicht der Hauptfokus. Read-Receipts
brauchen zusätzlich ein Schema (
message_readsexistiert nicht). Haben seit Monotoner Cursor ein Zuhause: sie werdenkind-Werte inconversation_events, das Log trägt sie ohne Umbau mit. Wird spezifizierbar, sobald Dumb-Pipe-Event & Backfill-Endpunkt steht. Ausnahme: die Teilnehmerliste eines Voice-Rooms ist nicht dasselbe wie Chat-Presence und bleibt im Scope von Voice-Signaling-Protokoll. - Verhalten beim Mobile-Resume aus dem Hintergrund. Was genau passiert, wenn die App nach Minuten oder Stunden zurückkommt — Reconnect-Backoff, Cursor-Sprung, UI-Zustand. Hängt an Dumb-Pipe-Event & Backfill-Endpunkt und Verbindungsaufbau & Auth über WS.
- Rate-Limiting auf Handshakes. Verbindungsaufbau & Auth über WS hat das Limit pro Nutzer entschieden (acht, älteste weicht), ein Rate-Limit auf Verbindungsversuche aber bewusst offengelassen: es trifft ein anderes Missbrauchsmuster und ist ohne Betriebserfahrung nicht kalibrierbar — ein Reconnect-Sturm nach einem Neustart sieht aus wie ein Angriff. Wird scharf, sobald es Zahlen aus dem Betrieb gibt.
- Observability des WS-Layers. Was misst man an offenen Verbindungen, Registry-Größe,
Zustellverzögerung? Seit Fan-out-Architektur gibt es zwei konkrete Pflichtmetriken statt
einer vagen Frage: die Füllung der Postgres-Notify-Queue (läuft sie voll, scheitern
Transaktionen beim Commit) und Reconnects des
PgListener(jeder löst einen Resync-Hinweis an alle Verbindungen aus). Der Rest ist noch nicht scharf. - Auth-UI bei N Servern. N Server heißt N Refresh-Zyklen und N Ablaufzeitpunkte — wie
sieht die Oberfläche aus, wenn Server C eine Neuanmeldung braucht, während A und B laufen?
Die Speicherfrage ist durch Wo lebt die WS-Verbindung im Client geklärt (Liste im Store,
Geheimnisse hinter dem
SecretStore-Trait); die UI-Seite hängt an Verbindungsaufbau & Auth über WS. - Rollenmodell für Conversations und Voice-Rooms. Erklärtes Ziel: Rollen sollen später
steuern, welche Conversations und welche Voice-Rooms für wen sichtbar und betretbar sind.
Voice-Signaling-Protokoll hat dafür die Naht gesetzt — Autorisierung läuft durch genau eine
Funktion, damit kein raumspezifisches Modell entsteht. Das Modell selbst ist noch nicht
scharf:
workspace_members.rolekennt heute nurowner/admin/member, ein Berechtigungsbegriff fehlt völlig. - Mobile-Plattform-Setup vor der Implementierung.
src-tauri/gen/androidundgen/appleexistieren noch nicht — Mobile ist nie initialisiert worden. Dazu die sieben Experimente aus §8 der WebRTC-Recherche, allen voran E1 (getUserMediaauf einem physischen iPhone, je einmal mit und ohne Info.plist-Key) und E2 (MODIFY_AUDIO_SETTINGSwirklich zwingend?). Blockiert die Spec-Arbeit nicht, gehört aber vor die Implementierung — vermutlich als Task-Ticket im Umsetzungs-Effort.
Out of scope
- Vollständiges E2EE-Design — Schlüsselverwaltung, Multi-Device-Sync (Desktop + Mobile
parallel ist gefordert), Rotation, Gruppen-Mitgliederwechsel, Geräteverlust,
Attachment-Verschlüsselung. Eigener Folge-Effort in der Größenordnung dieses hier.
In dieser Map steckt davon nur, was das Protokoll heute festlegen muss.
Neu bekannte Kopplung (14.08.2026): sicheres Voice hängt am E2EE-Effort. Der
Signaling-Server kann DTLS-Fingerprints tauschen (RFC 8827), und der Gegenmechanismus
verlangt, den Fingerprint signiert durch einen E2EE-Kanal zu übertragen — den es heute
nicht gibt. Voice-Signaling-Protokoll legt die Naht fest, kann sie aber nicht einlösen.
Voice ist damit erst nach dem E2EE-Effort gegen den Betreiber geschützt.
Zweite Kopplung:
voice_rooms.nameist nutzererzeugter Inhalt und fiele unter die Blob-Regel — aber ein neues Workspace-Mitglied müsste die Raumliste lesen können, ohne vorher Schlüsselmaterial zu haben. Ein Schlüsselverwaltungsproblem, das dort gelöst gehört. - Audio-Übertragung selbst — WebRTC-Peers, Codecs, Echo-Cancellation, SFU-Betrieb. Dieser Effort endet beim Signaling.
- Push-Notifications / Background-Delivery auf Mobile — FCM/APNs, Delivery-Queue, Wakeup-Semantik. Orthogonale Infrastruktur.
- Föderation (Server-zu-Server) — bewusste Produktentscheidung: isolierte Instanzen sind das Differenzierungsmerkmal gegenüber Matrix, nicht der Rückstand. Der Client verbindet sich zu mehreren Servern; die Server reden nie miteinander.
- Wahl der
SecretStore-Implementierung — Wo lebt die WS-Verbindung im Client hat die Naht festgelegt (Tokens und Refresh-Tokens hinter einem Trait mit zwei Methoden), die Implementierung aber bewusst an den E2EE-Effort abgegeben: der hat dasselbe Problem für Geräteschlüssel, in verschärfter Form. OS-Keychain ist der naheliegende Kandidat, hat aber auf Linux die schwächste Stelle (Secret Service braucht gnome-keyring oder KWallet). - Portable Schlüssel-Identität (Schlüsselpaar statt Server-Account als Identität) — reizvoll im E2EE-Kontext, aber eine Entscheidung des E2EE-Efforts. Lässt sich später ergänzen, ohne den WS-Layer zu brechen.
- Mehr-Instanz-Betrieb — Sticky Sessions, Load-Balancer-Konfiguration, Deployment. Im Scope ist nur die Trait-Naht, die den späteren Wechsel ermöglicht.
- Serverseitige Suche — unter E2EE ohnehin tot; braucht clientseitige Indizes.
- Metadaten-Minimierung (Sealed Sender, Padding) — widerspricht direkt der Entscheidung, Read-Receipts und Presence überhaupt zu wollen.
- TURN-Betrieb (coturn-Konfiguration, Credential-Rotation) — optionale externe Abhängigkeit, nicht Teil der Spec.