Architecture¶
The picture¶
graph TD
B["Browser<br/>Svelte SPA + protocol.js"]
K["Backend<br/>Litestar"]
G["Galene<br/>pinned upstream, unpatched"]
B -- "/api/v1/ + chat WebSocket" --> K
B -- "/ws signalling, then WebRTC media" --> G
K -- "operator /ws (op permission)<br/>presence in, kick/lock/record out" --> G
classDef ours fill:#e8eaf6,stroke:#3f51b5,color:#1a237e
classDef dep fill:#f1f8e9,stroke:#689f38,color:#33691e
class B,K ours
class G dep
Three connections, and only one of them is ours to design:
| Connection | Protocol | Owned by |
|---|---|---|
| browser → backend | /api/v1/ REST + a chat WebSocket |
us — see API contracts |
| browser → SFU | Galene's /ws signalling, then WebRTC media |
upstream, unchanged |
| backend → SFU | Galene's /ws, joined as an operator |
upstream, unchanged |
Media never touches the backend. It goes browser ↔ SFU directly.
What each part owns¶
Galene (a dependency, not a component)¶
Media forwarding, bandwidth estimation, codec negotiation, the /ws
signalling protocol, group lifecycle. Pinned, built by make galene-bin,
configured by one group file. See SFU.
backend (Python / Litestar)¶
Identity and sessions, events and rooms as domain objects, join tickets (including the signed Galene token), chat storage and fanout, the admin surface, and the operator connections that watch each live room.
It is a reference implementation of /api/v1/. Swapping it out is
the point — see alt-backend.
front (Svelte 5 SPA)¶
The meeting UI. Talks /api/v1/ to the backend for everything except
media, and speaks Galene's /ws protocol directly via protocol.js.
Data flow: the golden path¶
sequenceDiagram
autonumber
participant B as Browser
participant K as Backend
participant G as Galene
B->>K: POST /api/v1/rooms/ephemeral
K-->>B: {event_id, slug} — no SFU call
B->>K: POST /api/v1/events/{id}/join
K->>G: open operator /ws for the room
Note over K: signs the Galene JWT locally
K-->>B: join ticket {ws_url, slug, token, chat_token, operator_id}
B->>G: /ws join with the token
Note over G: auto-subgroups materialises the room meetings/{slug}
G-->>B: joined
B->>G: WebRTC offer/answer + ICE
G-->>B: media, direct (never via the backend)
G-->>K: user add
Note over K: participant_joined → DB row → Pluggy hook
B->>K: POST /api/v1/events/{id}/chat
K-->>B: fanout over the backend's own WebSocket
The two things worth noticing: the HTTP join path never calls the SFU, and chat never reaches it — the SFU sees signalling and media, nothing else.
Why this shape¶
The SFU is the hard part, and it already exists. Forking it means owning media forwarding and WebRTC negotiation forever. Pinning it means owning a version number.
The contract is the product boundary. A host platform that already
has users, sessions and storage implements /api/v1/ and keeps the rest.
alt-backend/ proves the boundary is real rather than aspirational.
Chat is a backend concern, end to end. The frontend posts to the backend and the backend fans out over its own WebSocket; Galene's own chat protocol is left untouched and unused. This keeps meeting content out of the SFU entirely.
Further reading¶
notes/v3/upstream-gap-check.md— why there is no forknotes/v3/retrospective.md— what the fork experiment producednotes/v3/decisions.md— the running decision log (D1–D16)notes/v3/evidence/— reproducible proofs for the claims above