Matrix-Chat: Architektur­konzept & Umsetzungs­empfehlung

Multi-Tenant-Chat auf Basis von Matrix (Synapse) mit Keycloak-Authentifizierung — als Standalone-Frontend und als einbettbare Web-Component. Bewusst einfach gehalten, ohne spätere Optionen (Kubernetes, mehr Tenants, Video) zu verbauen.

Stand: 01.07.2026 · Entwurf zur Diskussion

0TL;DR — die Kernentscheidungen

FrageEmpfehlung
HomeserverEin gemeinsamer Synapse für alle Tenants, Föderation deaktiviert. Getrennte Homeserver nur, wenn Compliance es später erzwingt.
Raum-BindungEin Raum pro Vorgang (requestId), nicht pro Berater+Kunde-Paar.
Raum-ErstellungDer Orchestrator (Bot mit Power Level 100) erstellt Räume — nie die Nutzer selbst.
OrchestratorEigener, kleiner Service — weil mehrere Produkte den Chat nutzen sollen: ein Integrationspunkt für alle, statt Modul-Kopien in jedem Backend. Bewusst schmal gehalten (5 Endpunkte, nie im Nachrichtenpfad).
LoginToken-Exchange: Keycloak-Access-Token rein → Matrix-Access-Token raus (Orchestrator nutzt Synapse-Admin-API, provisioniert User beim ersten Mal). Ein Code-Pfad für Standalone und Embedded.
BerechtigungenKeycloak sagt, wer jemand ist (Rolle). Matrix Power Levels sagen, was er im Raum darf. Der Orchestrator übersetzt einmalig beim Erstellen/Einladen. Durchsetzung passiert serverseitig in Synapse, nicht in der UI.
Kunden-SchutzKunde = Power Level 0, invite erfordert PL 50, Räume sind invite-only. Damit sind Einladen/Moderieren auch per direkter API unmöglich.
Tenant-IsolationTenant-Präfix in Matrix-IDs (@acme.max:…), Orchestrator prüft Tenant bei jeder Aktion, User-Directory-Suche aus, plus kleines Synapse-Modul als harte Leitplanke.
SpacesSpäter Optional — für das MVP nicht nötig, die Raumliste kommt aus dem Sync bzw. dem Vorgangskontext.
E2EEBewusst aus im MVP: Support-/Audit-Fähigkeit, Bot-Zugriff und Einfachheit wiegen hier schwerer. Explizite Entscheidung, siehe §9.

1Technische Komponenten

Es braucht genau vier Bausteine — drei davon existieren schon oder sind Standardsoftware. Neu gebaut wird nur die Web-Component und das Orchestrator-Modul.

flowchart TB
    subgraph clients ["Clients"]
        SA["Standalone-Chat
(statische Shell-Seite
+ Web-Component)"] EMB["Bestehendes Produkt
(Web-Component
eingebettet)"] end subgraph products ["Produkt-Backends (mehrere)"] P1["Produkt A
(Vorgänge / Requests)"] P2["Produkt B
(Vorgänge / Requests)"] end ORCH["Chat-Orchestrator
(eigener kleiner Service, neu)"] P1 -- "REST + Service-Token:
Request angenommen" --> ORCH P2 -- "REST + Service-Token" --> ORCH KC["Keycloak
(vorhanden)
Realm pro Tenant"] SYN["Synapse Homeserver
(einer für alle Tenants,
keine Föderation)"] PG[("PostgreSQL")] PGO[("PostgreSQL
(Mapping + Standalone-Vorgänge)")] LK["LiveKit
(vorhanden, Video)"] SA -- "OIDC-Login" --> KC EMB -. "hat bereits
Keycloak-Session" .-> KC SA -- "Keycloak-Token" --> ORCH EMB -- "Keycloak-Token" --> ORCH ORCH -- "validiert Token
(JWKS)" --> KC ORCH -- "Admin-API: User anlegen,
Räume erstellen, einladen,
Matrix-Token ausstellen" --> SYN SA -- "Matrix Client-Server-API
(sync, senden, lesen)" --> SYN EMB -- "Matrix Client-Server-API" --> SYN SYN --- PG ORCH --- PGO ORCH -. "LiveKit-JWT
(später)" .-> LK
Gesamtarchitektur. Neu zu bauen sind nur Web-Component und Orchestrator-Service. Beliebig viele Produkte docken über dieselbe REST-API an; der Chat-Traffic selbst läuft direkt Client ↔ Synapse.
KomponenteWasStatus
SynapseMatrix-Homeserver, ein Deployment, Föderation aus, PostgreSQL. Erreichbar unter z. B. matrix.example.com (eine Domain für alle Tenants).Standardsoftware, nur konfigurieren
KeycloakBleibt einzige Identitätsquelle. Pro Tenant ein Realm (passt zum Subdomain-Modell). Drei neue Realm-Rollen, sonst keine Änderung.Vorhanden
Chat-OrchestratorEigener kleiner Service mit eigener DB. Übersetzt Produkt-Events + Keycloak-Rollen in Matrix-Operationen. Einziger Ort mit Synapse-Admin-Rechten; gemeinsamer Andockpunkt für alle Produkte.Neu (klein)
Web-ComponentEin Custom Element (z. B. Lit + matrix-js-sdk). Wird standalone in einer statischen Shell-Seite und embedded im Produkt verwendet — derselbe Code.Neu
LiveKitBleibt wie es ist. Einzige Berührung: der Orchestrator stellt LiveKit-JWTs aus und nutzt die Matrix-Room-ID als LiveKit-Raumname.Vorhanden, Randthema

2Die zwei Modi: Standalone & Embedded

Kernidee: Es gibt keinen Unterschied im Chat-Stack zwischen den Modi. Der einzige Unterschied ist, woher das Keycloak-Token kommt und welche Räume angezeigt werden.

Standalone-Modus

  • Statische Seite pro Tenant-Subdomain (z. B. chat.acme.example.com), die nur die Web-Component hostet.
  • Die Shell macht selbst den OIDC-Redirect-Login gegen das Realm des Tenants (aus der Subdomain abgeleitet).
  • Danach: Keycloak-Token → Orchestrator → Matrix-Token (wie embedded).
  • Raumliste = alle Räume aus dem Matrix-Sync. Kein Produkt nötig.
  • Ohne bestehenden Vorgang: Button „Neue Anfrage“ → Orchestrator legt einen leichtgewichtigen Vorgang an + Raum, Berater-Seite sieht ihn in einer Eingangs-Ansicht und nimmt ihn an.

Embedded-Modus

  • Produkt bindet <chat-widget> ein und übergibt eine Token-Callback (das Produkt hat die Keycloak-Session bereits).
  • Widget zeigt genau den Raum zum aktuellen Vorgang (request-id-Attribut) — oder die Raumliste des Nutzers, je nach Einbettungsort.
  • Kein zweiter Login, kein iframe-Redirect-Gefrickel: das Token-Exchange ist ein einfacher XHR-Call.
<chat-widget
  orchestrator-url="https://chat-api.example.com"
  request-id="4711">
</chat-widget>

widget.getKeycloakToken = () =>
  keycloak.token; // Host-App liefert
Entscheidung: Web-Component als „dummes“ UI über der Matrix Client-Server-API + wenigen Orchestrator-Endpunkten. Alles, was Berechtigungen betrifft, entscheidet der Server — die Component blendet nur aus, was der eigene Power Level nicht erlaubt (aus m.room.power_levels ablesbar, kein eigenes Rechte-Modell im Frontend).

3Rollenmodell & Keycloak

Drei Realm-Rollen genügen. Der Tenant ist implizit durch das Realm (Subdomain) gegeben und wird als Claim mitgegeben — kein eigenes Tenant-Attribut pro User nötig.

Keycloak-RolleWerDarf (fachlich)
chat-userKunden / Gäste (echte Keycloak-User mit wenig Rechten)In eigenen Vorgangs-Räumen lesen und schreiben. Sonst nichts: kein Einladen, kein Raum erstellen, keine Moderation, keine Nutzersuche.
chat-beraterBeraterAlles von chat-user, plus: Vorgänge annehmen (→ Raum entsteht), weitere Berater in eigene Räume holen, Nachrichten löschen (redact), Kunden aus dem Raum entfernen.
chat-berater-adminBerater-AdminsAlles von chat-berater, plus: Berater auch in fremde Räume des Tenants setzen, Räume schließen/archivieren.

Token-Claims

Der Orchestrator braucht aus dem Access-Token nur:

{
  "sub": "8f3a…",                        // stabile User-ID → Basis der Matrix-ID
  "preferred_username": "m.mustermann",
  "iss": "https://auth.example.com/realms/acme",  // → Tenant "acme"
  "realm_access": { "roles": ["chat-berater"] }
}
Wichtig — Grenze von Keycloak: Keycloak-Rollen sind hervorragend geeignet, um global zu bestimmen, was ein Nutzertyp darf (schreiben, einladen, moderieren, Admin). Sie sind nicht geeignet, um pro Raum zur Laufzeit Rechte durchzusetzen — das kann und soll Matrix selbst über Power Levels. Also: Keycloak = Identität + Rollentyp, Matrix = Durchsetzung im Raum, Orchestrator = einmalige Übersetzung dazwischen. Keine Berechtigungs-Logik doppelt pflegen.

4Übersetzung: Keycloak-Rolle → Matrix-Rechte

Die Übersetzung passiert an genau zwei Stellen: beim Token-Exchange (User-Provisionierung) und beim Erstellen/Einladen in Räume (Power Level setzen). Danach arbeitet Matrix autark.

KeycloakMatrix-MechanismusWert
chat-user (Kunde)Power Level im Raum0 — Nachrichten senden ja, sonst nichts
chat-beraterPower Level im Raum50 — invite, kick, redact
chat-berater-adminKein höherer PL! Admin-Aktionen laufen über den Orchestrator-Bot50 im Raum; Sonderrechte nur via Orchestrator-API
Orchestrator-BotPower Level100 — Raum-Owner, setzt State, lädt ein, archiviert
Mitgliedschaft im Vorgangm.room.member (Invite durch Bot)Nur wer fachlich zum Vorgang gehört, wird eingeladen
„Wer darf rein?“m.room.join_rulesinvite — niemand kann selbst joinen
Verlauf für später hinzugezogene Beraterm.room.history_visibilityshared — Übergabe/Vier-Augen funktioniert mit vollem Kontext
Matrix-„Guests“m.room.guest_access + Serverconfigforbidden / global deaktiviert. Unsere Gäste sind echte Keycloak-User — das Matrix-Guest-Feature wird nicht benutzt.
Entscheidung — warum Berater-Admin keinen PL 100 bekommt: PL-100-User könnten das Power-Level-Schema selbst umbauen und das Konzept aushebeln. Admin-Funktionen („Berater in fremden Raum setzen“, „Raum schließen“) sind fachliche Operationen → sie laufen als Orchestrator-Endpunkte, die die Keycloak-Rolle prüfen und dann als Bot handeln. Das hält das Raum-Template invariant.

Rollenänderungen

Verliert ein User die Berater-Rolle, wird das beim nächsten Token-Exchange erkannt; der Orchestrator senkt dann seine Power Levels bzw. entfernt ihn aus Räumen (MVP: lazy beim Login; später: Keycloak-Event-Listener/Webhook für sofortige Wirkung).

5Raum-Template & Power-Level-Konzept

Jeder Raum hängt am Vorgang, nicht am Personen-Paar. Alias-Schema: #<tenant>.req-<requestId>:matrix.example.com. Der Orchestrator merkt sich nur das Mapping requestId ↔ roomId — mehr Schatten-Datenhaltung braucht es nicht.

Warum Raum pro requestId statt Berater+Kunde?
  • Übergabe: Beraterwechsel = Membership-Änderung, der Raum (und die Historie) bleibt.
  • Mehrere Berater am selben Fall sind trivial — beim Personen-Paar-Modell bräuchte man dafür neue Räume.
  • Audit: Die gesamte Kommunikation zu einem Fall ist an einem Ort.
  • Mehrere Fälle desselben Kunden bleiben sauber getrennt.
  • Ein Kunde mit zwei Vorgängen hat zwei Räume — das ist gewollt, nicht redundant.

createRoom-Template (vom Bot ausgeführt)

{
  "preset": "private_chat",
  "room_alias_name": "acme.req-4711",
  "name": "Baufinanzierung #4711",
  "creation_content": { "m.federate": false },
  "initial_state": [
    { "type": "m.room.join_rules",         "content": { "join_rule": "invite" } },
    { "type": "m.room.history_visibility", "content": { "history_visibility": "shared" } },
    { "type": "m.room.guest_access",       "content": { "guest_access": "forbidden" } },
    { "type": "com.example.request",       "content": { "tenant": "acme", "requestId": "4711" } }
  ],
  "power_level_content_override": {
    "users":          { "@chat-bot:matrix.example.com": 100,
                        "@acme.b.schmidt:matrix.example.com": 50 },
    "users_default":  0,
    "events_default": 0,        // PL 0 darf Nachrichten senden → Kunde kann schreiben
    "state_default":  100,      // Raum-Einstellungen nur der Bot
    "invite": 50, "kick": 50, "ban": 50, "redact": 50,
    "events": {
      "m.room.name": 100, "m.room.topic": 100,
      "m.room.avatar": 100, "m.room.power_levels": 100
    }
  },
  "invite": [ "@acme.b.schmidt:matrix.example.com",
              "@acme.k.mueller:matrix.example.com" ]
}

Das Custom-State-Event com.example.request verankert den fachlichen Kontext im Raum selbst — nützlich für das Widget (Titelzeile, Deep-Link zum Vorgang) und für Betrieb/Debugging.

Relevante Matrix-Mechanismen im Überblick

MechanismusRolle im Konzept
Power LevelsDas eigentliche Berechtigungssystem im Raum. Serverseitig durchgesetzt — gilt für jede API-Nutzung, nicht nur fürs eigene Frontend.
Join Rules (invite)Niemand betritt einen Raum ohne Einladung. Einladungen kann nur PL ≥ 50 (Berater/Bot) aussprechen.
History Visibilityshared: hinzugezogene Berater sehen den bisherigen Verlauf.
Room AliasesDeterministische Adresse pro Vorgang, Idempotenz bei Raum-Erstellung.
Custom State EventsFachlicher Kontext (Tenant, requestId) direkt am Raum.
Admin-API / Login-as-UserOrchestrator provisioniert User und stellt Matrix-Tokens aus, ohne Passwörter.
Spam-Checker-Modul-APIServerweite Leitplanken (wer darf Räume erstellen / einladen), siehe §9.
SpacesSpäter Optionale Gruppierung „alle Räume eines Tenants“ für Berater-Übersichten. Kein Sicherheitsfeature — Isolation läuft nicht über Spaces.
FöderationAus Geschlossenes System, m.federate: false + Serverconfig.

6Der Chat-Orchestrator

Der Orchestrator ist die einzige Stelle, an der Produkte, Keycloak und Matrix zusammenkommen. Er ist bewusst klein: fünf Endpunkte, ein Bot-User, zwei DB-Tabellen.

Entscheidung: Eigener kleiner Service. Der Chat wird von mehreren Produkten genutzt — das entscheidet die Frage. Als Modul müsste die Logik in jedes Backend kopiert werden (oder ein Produkt würde zum Chat-Hub für alle anderen, eine unglückliche Kopplung). Als Service gibt es einen Andockpunkt mit einer versionierten REST-API, an den jedes Produkt gleich anbindet — und der Standalone-Modus braucht ohnehin kein Produkt-Backend. Weitere Vorteile: Chat-Releases sind von Produkt-Releases entkoppelt, die Synapse-Admin-Credentials liegen an genau einer Stelle, und für Kubernetes ist ein eigener Container ohnehin der Normalfall.
Damit „eigener Service“ nicht „großer Service“ wird: Der Orchestrator bekommt keine Fachlogik. Vorgänge, Zuständigkeiten, Workflows bleiben in den Produkten — der Orchestrator kennt nur tenant + requestId + Teilnehmer. Er hält eine eigene kleine PostgreSQL-DB mit zwei Tabellen: room_mapping (requestId ↔ roomId) und standalone_requests (die leichtgewichtigen Vorgänge aus dem Standalone-Modus). Mehr nicht — sonst entsteht ein zweites Fachbackend.

Anbindung der Produkte (Machine-to-Machine)

Jedes Produkt-Backend erhält einen Keycloak-Service-Account (Client-Credentials-Flow) und ruft den Orchestrator per REST auf — z. B. beim „Request angenommen“. Der Orchestrator prüft am Service-Token, welches Produkt und welcher Tenant aufruft. Kein Event-Bus, kein Message-Broker: ein synchroner, idempotenter REST-Call reicht für diese Frequenz völlig; das Produkt kann ihn bei Fehlern einfach wiederholen.

Aufgaben & API

EndpunktAuthFunktion
POST /api/chat/tokenKeycloak-BearerToken-Exchange: validiert das Keycloak-Token (JWKS), provisioniert den Matrix-User beim ersten Mal (@<tenant>.<username>:…), stellt via Admin-API ein Matrix-Access-Token aus. Antwort: { mxid, accessToken, homeserverUrl }.
POST /api/chat/roomsService-Token (Produkt-Backend)Idempotent: Raum zu requestId erstellen oder zurückgeben (Alias-Lookup). Wird vom Produkt beim „Request annehmen“ aufgerufen.
POST /api/chat/rooms/{requestId}/participantsBerater / Berater-AdminWeiteren Berater einladen. Prüft: Aufrufer ist Berater desselben Tenants und Mitglied des Raums (oder Berater-Admin). Bot lädt ein und setzt PL 50.
GET /api/chat/colleaguesBeraterBerater-Verzeichnis des Tenants für den Einladen-Dialog (via Keycloak-Admin-API nach Rolle chat-berater gefiltert — die Matrix-Nutzersuche bleibt aus, und es braucht kein Produkt dafür).
POST /api/chat/rooms/{requestId}/closeBerater-AdminVorgang abgeschlossen: Bot kickt Teilnehmer bzw. archiviert.

Später POST /api/chat/rooms/{requestId}/call-token — LiveKit-JWT für Videocall, Raumname = Matrix-Room-ID, Berechtigung = Matrix-Mitgliedschaft. Mehr Kopplung braucht LiveKit nicht.

Was der Orchestrator bewusst nicht tut: Nachrichten proxien (der Chat-Traffic läuft direkt Client ↔ Synapse — der Orchestrator ist nie im heißen Pfad) und Fachlogik abbilden (Vorgänge gehören den Produkten).

7Die konkreten Flows

7.1 Login / Token-Exchange (beide Modi)

sequenceDiagram
    autonumber
    participant U as Browser (Widget)
    participant KC as Keycloak
    participant O as Orchestrator
    participant S as Synapse

    alt Standalone
        U->>KC: OIDC-Redirect-Login (Realm aus Subdomain)
        KC-->>U: Access-Token
    else Embedded
        Note over U: Produkt hat Keycloak-Session,
liefert Token per Callback end U->>O: POST /api/chat/token (Bearer: Keycloak-Token) O->>O: Token validieren (JWKS),
Tenant + Rollen extrahieren opt Erster Login O->>S: Admin-API: User @acme.k.mueller anlegen end O->>S: Admin-API: Access-Token für User ausstellen S-->>O: Matrix-Access-Token O-->>U: { mxid, accessToken, homeserverUrl } U->>S: /sync — Chat läuft ab jetzt direkt

7.2 Berater nimmt Vorgang an → Raum entsteht

sequenceDiagram
    autonumber
    participant B as Berater (Produkt-UI)
    participant P as Produkt-Backend
    participant O as Orchestrator (Service)
    participant S as Synapse

    B->>P: Request #4711 annehmen
    P->>P: Fachliche Zuweisung
    P->>O: POST /api/chat/rooms (Service-Token)
{ requestId, beraterId, kundeId } O->>S: Alias #acme.req-4711 auflösen alt Raum existiert noch nicht O->>S: Bot: createRoom (Template §5)
PLs: Bot 100, Berater 50, Kunde 0 O->>O: Mapping requestId ↔ roomId speichern end O->>S: Bot: invite Berater + Kunde S-->>B: Raum erscheint im Sync des Beraters Note over S: Kunde sieht den Raum beim
nächsten Öffnen des Chats

7.3 Weiterer Berater wird hinzugefügt

sequenceDiagram
    autonumber
    participant B as Berater A (Widget)
    participant O as Orchestrator
    participant S as Synapse
    participant B2 as Berater B

    B->>O: GET /api/chat/colleagues (Einladen-Dialog)
    O-->>B: Berater-Liste des Tenants
    B->>O: POST /rooms/4711/participants { userId: B2 }
    O->>O: Prüfen: A ist chat-berater, gleicher Tenant,
Mitglied im Raum (oder berater-admin) O->>S: Bot: invite @acme.b2 + Power Level 50 setzen S-->>B2: Einladung im Sync → Auto-Join im Widget Note over B2: Sieht dank history_visibility=shared
den bisherigen Verlauf

Der Kunde hat diesen Dialog nie: das Widget blendet ihn bei PL < 50 aus, der Orchestrator lehnt den Call ohne Berater-Rolle ab, und Synapse würde einen direkten /invite mit PL 0 ohnehin mit 403 beantworten — dreifach dicht, entscheidend sind die beiden Server-Schichten.

7.4 Standalone ohne bestehendes Produkt

sequenceDiagram
    autonumber
    participant K as Kunde (Standalone-Chat)
    participant O as Orchestrator
    participant S as Synapse
    participant B as Berater (Standalone/Produkt)

    K->>O: „Neue Anfrage“ (Thema optional)
    O->>O: Leichtgewichtigen Vorgang anlegen
(Status: offen, kein Berater) O->>S: Bot: Raum erstellen, Kunde einladen Note over B: Eingangs-Ansicht zeigt offene Anfragen
(Orchestrator-Query, kein Matrix nötig) B->>O: Anfrage annehmen O->>S: Bot: Berater einladen, PL 50 K-->>B: Chat läuft — identisch zu 7.2

So bleibt die Regel „Räume hängen an Vorgängen“ auch ohne Produkt erhalten — der Vorgang ist dann eben minimal (ID, Tenant, Status, Thema). Wächst später ein Produkt darum, ist nichts umzubauen.

8Durchsetzung: Warum Kunden auch per API nichts können

UI-Ausblenden ist Komfort, keine Sicherheit. Die Durchsetzung hat drei Schichten — die untersten zwei sind serverseitig und gelten für jeden API-Client, auch für einen Kunden mit curl und seinem eigenen Matrix-Token.

SchichtMechanismusVerhindert
1 — Widget (Komfort)Buttons nur bei ausreichendem Power Level rendernVerwirrung, nicht Missbrauch
2 — Synapse Power Levels & Join Rulesinvite/kick/ban/redact ≥ 50, State ≥ 100, Räume invite-onlyEinladen, Moderieren, Raum-Manipulation, Fremd-Join — per jeder API
3 — Synapse-Modul (Leitplanke)Kleines Python-Modul über die Spam-Checker-Hooks:
user_may_create_room → nur der Bot
user_may_invite → nur Bot; zusätzlich: Einlader- und Eingeladenen-MXID müssen denselben Tenant-Präfix tragen
Kunden erstellen eigene Räume; Cross-Tenant-Einladungen; Umgehung des Orchestrators
flankierenduser_directory.search_all_users: false, Presence aus, Föderation aus, Rate-Limits, Matrix-Guest-Accounts deaktiviertAusspähen anderer Nutzer/Tenants
Explizite Entscheidung nötig — E2EE: Ende-zu-Ende-Verschlüsselung im MVP ausschalten. Begründung: Der Bot muss Räume verwalten, Berater-Übergaben brauchen lesbare Historie, Support/Compliance brauchen Zugriff, und E2EE-Schlüsselverwaltung in einer einbettbaren Web-Component ist der mit Abstand größte Komplexitätstreiber im ganzen Vorhaben. Transportverschlüsselung (TLS) + geschlossener, förderationsfreier Server ist für diesen Anwendungsfall (Berater ↔ Kunde, geschäftlicher Kontext) das übliche Modell. Falls E2EE regulatorisch gefordert wird, ist das ein eigenes Projekt — dann von Anfang an einplanen.

9Multi-Tenancy & Isolation

flowchart LR
    subgraph acme ["Tenant acme — acme.example.com"]
        KCA["Keycloak-Realm acme"]
        UA["@acme.k.mueller
@acme.b.schmidt"] end subgraph beta ["Tenant beta — beta.example.com"] KCB["Keycloak-Realm beta"] UB["@beta.k.weber
@beta.b.krause"] end subgraph syn ["Ein Synapse — matrix.example.com"] RA["#acme.req-4711
#acme.req-4712"] RB["#beta.req-0815"] MOD["Synapse-Modul:
Invites nur innerhalb
desselben Tenant-Präfixes"] end UA --> RA UB --> RB UA -. "✗ blockiert" .-x RB MOD --- RA MOD --- RB
Logische Isolation auf einem gemeinsamen Homeserver: Tenant-Präfix in jeder Matrix-ID und jedem Alias, durchgesetzt vom Orchestrator und vom Synapse-Modul.

Ein Homeserver oder einer pro Tenant?

Gemeinsamer Synapse EmpfehlungSynapse pro Tenant
Betrieb1 × deployen, upgraden, monitoren, backuppenn × alles; Provisionierung neuer Tenants wird ein Projekt
IsolationLogisch (Präfixe + Modul + invite-only). Für B2B-SaaS üblich und ausreichend.Physisch — nur nötig bei harten Compliance-Vorgaben (eigene DB/Keys pro Kunde)
KostenEine kleine Instanz trägt viele TenantsGrundlast pro Tenant
Später wechselbar?Ja — weil MXIDs Tenant-Präfixe tragen und der Orchestrator die einzige Admin-Schnittstelle ist, kann ein einzelner Tenant später auf eine eigene Instanz migriert werden, ohne das Konzept zu ändern.

Isolation konkret:

Matrix Spaces? Optional, kein Bestandteil der Isolation. Sinnvoll erst, wenn Berater viele Räume haben und eine gruppierte Übersicht brauchen („alle offenen Fälle“). Das MVP löst das über die Vorgangs-Ansicht des Produkts bzw. die Eingangs-Ansicht des Orchestrators.

10Kubernetes-Fähigkeit (ohne es jetzt zu bauen)

Nichts am Konzept setzt Kubernetes voraus — und nichts verhindert es. Diese Regeln jetzt einhalten, dann ist der Umzug später ein Deployment-Thema, kein Architektur-Thema:

11MVP-Empfehlung & Roadmap

MVP — bewusst schmal

  1. Synapse (Monolith) + PostgreSQL, Föderation/Guests/Registrierung aus, Directory-Suche aus.
  2. Orchestrator als eigener kleiner Service: die 5 Endpunkte aus §6, Bot-User, eigene DB (room_mapping, standalone_requests), Service-Accounts für die Produkt-Backends.
  3. Token-Exchange-Login (Keycloak → Matrix) für beide Modi.
  4. 3 Keycloak-Rollen: chat-user, chat-berater, chat-berater-admin.
  5. Raum-Template aus §5, Raum pro requestId, PLs 100/50/0.
  6. Web-Component: Raumliste, ein Raum, Text senden, Bilder/Dateien, „Kollege hinzufügen“-Dialog (nur Berater).
  7. Standalone-Shell-Seite mit OIDC-Login + „Neue Anfrage“-Flow.
  8. Synapse-Modul: Raum-Erstellung nur Bot, Invites nur innerhalb des Tenants.

Später — wenn der Bedarf da ist

  • LiveKit-Anbindung im Chat: Call-Button, Orchestrator stellt LiveKit-JWT aus, Raumname = Matrix-Room-ID.
  • Spaces pro Tenant für Berater-Übersichten.
  • Sofortige Rollen-Synchronisation via Keycloak-Event-Listener (statt lazy beim Login).
  • Push-Benachrichtigungen / E-Mail-Nudges („Ihr Berater hat geantwortet“).
  • Archivierung/Export abgeschlossener Vorgänge (Compliance).
  • Matrix Authentication Service (OIDC-nativ): ersetzt den Token-Exchange, wenn das Matrix-Ökosystem dort stabil angekommen ist — der Wechsel ist auf den Orchestrator begrenzt.
  • Synapse-Worker / K8s-Deployment bei Lastwachstum.
  • Dedizierter Homeserver für einzelne Compliance-Tenants.
  • E2EE, falls regulatorisch gefordert — als eigenes Projekt.
Warum das einfach bleibt: Ein Homeserver, ein kleiner neuer Service, eine neue UI-Komponente — und jedes weitere Produkt bindet nur noch die Web-Component ein und ruft eine REST-API. Kein Message-Proxy, keine doppelte Rechteverwaltung, keine Fachlogik im Chat-Service, keine E2EE-Schlüsselverwaltung. Jede „Später“-Position ist additiv — nichts davon erfordert Umbau am MVP.

12Checkliste: alle Ausgangsfragen in einem Satz

Welche Komponenten brauchen wir?
Synapse, Keycloak (vorhanden), Orchestrator-Modul, Web-Component — LiveKit bleibt am Rand (§1).
Wie funktioniert Standalone?
Statische Shell mit OIDC-Login + dieselbe Web-Component; Anfragen ohne Produkt über den „Neue Anfrage“-Flow (§2, §7.4).
Wie funktioniert Embedded?
Produkt liefert das Keycloak-Token per Callback, Widget tauscht es beim Orchestrator gegen ein Matrix-Token und zeigt den Raum zum Vorgang (§2, §7.1).
Wer erstellt Räume?
Immer der Orchestrator-Bot, ausgelöst durch „Request angenommen“ (§7.2).
Raum an requestId oder an Berater+Kunde?
An die requestId — wegen Übergaben, Mehrfach-Beratern und Audit (§5).
Wie kommt ein weiterer Berater in den Raum?
Über den Orchestrator-Endpunkt mit Tenant- und Rollenprüfung; der Bot lädt ein und setzt PL 50 (§7.3).
Rolle des Orchestrators? Als Modul im bestehenden Backend?
Übersetzer Produkte/Keycloak → Matrix, nie im Nachrichtenpfad. Als eigener kleiner Service, weil mehrere Produkte den Chat nutzen — ein Andockpunkt statt Modul-Kopien; bewusst ohne Fachlogik gehalten (§6).
Keycloak-Rollen sinnvoll? Geeignet für Chat-Funktionen?
Drei Realm-Rollen; geeignet für die globale Frage „was für ein Nutzer ist das“, nicht für Raum-Laufzeitrechte — das machen Power Levels (§3, §4).
Wie übersetzt man Keycloak → Matrix?
Beim Token-Exchange und beim Raum-Erstellen/Einladen: Rolle → Power Level (0/50), Bot 100, invite-only, history shared (§4, §5).
Wie sind Kunden auch API-seitig beschränkt?
Power Levels + Join Rules serverseitig, plus Synapse-Modul gegen Raum-Erstellung/Fremd-Invites — die UI ist nur die dritte, kosmetische Schicht (§8).
Tenant-Isolation?
Präfix in MXIDs/Aliassen, Realm-basierte Tenant-Ermittlung, invite-only, Modul-Check, keine Suche/Presence (§9).
Gemeinsamer oder getrennter Homeserver?
Gemeinsam; getrennte Instanzen bleiben pro Tenant nachrüstbar (§9).
Spaces?
Optional, später, kein Isolationsmechanismus (§9).
Kubernetes?
Zustandsloser Orchestrator, Env-Config, externes Postgres, S3-Media — dann ist K8s reines Deployment (§10).
Wie bleibt es simpel?
Ein Homeserver, ein Modul, eine Komponente, kein Proxy, kein E2EE im MVP, alles Weitere additiv (§11).