Skip to content

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 fork
  • notes/v3/retrospective.md — what the fork experiment produced
  • notes/v3/decisions.md — the running decision log (D1–D16)
  • notes/v3/evidence/ — reproducible proofs for the claims above