API contracts¶
One contract matters: /api/v1/. Everything else is either
upstream Galene's protocol or has no HTTP surface at all.
| Boundary | Caller | Callee | Auth |
|---|---|---|---|
/api/v1/ |
Frontend | Backend | Session cookie or guest |
| browser ↔ SFU | Frontend | Galene | Signed JWT |
| backend ↔ SFU | Backend | Galene | Operator /ws, no HTTP |
The authoritative wire spec for /api/v1/ is notes/v2/api-contracts.md
in the repo; its /sfu-api/v1/ and /sfu-events/ sections are marked
historical. This page is a tour.
/api/v1/ — frontend ↔ backend¶
The frontend speaks this. The reference implementation is in
backend/; a minimum alt is in
alt-backend/.
Endpoints the Svelte frontend calls¶
| Method | Path | Returns | Purpose |
|---|---|---|---|
GET |
/api/v1/config |
{app_name, environment, auth, sfu: {ws_url}} |
Public runtime config |
GET |
/api/v1/me |
MeResponse or 401 |
Current user (401 → guest) |
POST |
/api/v1/auth/login |
{token} |
Local-password login |
POST |
/api/v1/auth/logout |
204 | Clear session |
POST |
/api/v1/rooms/ephemeral |
{event_id, slug, join_url} |
Create an ad-hoc room |
GET |
/api/v1/events/by-slug/{slug} |
EventSummary |
Resolve slug → event_id |
POST |
/api/v1/events/{id}/join |
JoinTicket |
Mint a join ticket |
POST |
/api/v1/events/{id}/chat |
ChatMessage |
Post a chat message |
GET |
/api/v1/events/{id}/chat-ws |
WebSocket | Chat fanout |
GET |
/api/v1/events/{id}/participants |
[ParticipantResponse] |
Backend's view of attendees |
The shapes are generated from the backend's OpenAPI spec — hit
http://localhost:8000/schema/openapi.json when the backend is
running. The frontend consumes them via openapi-typescript into
front/src/lib/api/schema.d.ts.
Auth model¶
- Session cookie (
credentials: include) for authenticated users. Set by/auth/login, cleared by/auth/logout. - Guest — no credentials, no session.
/mereturns 401. Most endpoints work without a session.
The join ticket¶
{
"ws_url": "ws://localhost:5173/ws",
"slug": "meetings/abc123",
"token": "<JWT the SFU will accept>",
"username": "Alice",
"chat_token": "<separate short-lived JWT>",
"event_id": "…",
"operator_id": "operator-1a2b3c4d"
}
slug is the full Galene group name, not the bare room slug.
operator_id names the backend's own bot in that room so the frontend
can hide it from the roster.
What's intentionally NOT here¶
- Room management. There is nothing to manage: rooms are implicit.
- Media. It goes browser ↔ SFU directly and never crosses this API.
Backend ↔ SFU — no HTTP contract¶
There isn't one, and that is the point. Against stock Galene the backend needs no control-plane API:
| What the backend needs | How it gets it |
|---|---|
| A room to exist | It already does — auto-subgroups on the parent group |
| A join token | Signs it locally against the group's authKeys |
| Who is in a room | Its own operator /ws connection, from Galene's user stream |
| Kick / lock / record | The same connection — these are /ws protocol messages, not endpoints |
| A room to go away | It evaporates when the last client leaves |
Earlier revisions of this project defined a /sfu-api/v1/ control plane
and an outbound /sfu-events/ channel, both requiring a patched SFU.
Neither exists any more. notes/v3/upstream-gap-check.md records why.
Upstream's own /galene-api/v0/ is available for configuration and
inspection (groups, users, tokens, .stats), but Beep reads none of it
today. Note that .stats carries no usernames and upstream documents its
format as unstable, which is why presence comes from the operator
connection instead.
The operator connection¶
backend/src/app/sfu/operator.py. One /ws connection per active room,
joined with the op permission, opened on first join and released when
the roster empties.
Inbound, it consumes Galene user frames and turns them into the
envelope the dispatcher already understood:
{
"seq": 1,
"ts": "2026-08-31T09:12:03.417Z",
"kind": "participant_joined",
"slug": "meetings/abc123",
"data": {"participant_id": "…", "username": "Alice", "permissions": ["present"]}
}
kind is one of participant_joined, participant_left, room_empty.
Outbound, it sends useraction/kick and groupaction/lock|record.
Kick is asynchronous
kick only puts a frame on the wire; the roster updates when Galene
echoes user delete. SfuAdmin.kick waits for that confirmation
before returning, so an admin UI re-rendering straight afterwards
doesn't show the victim still present.
/ws — browser ↔ SFU¶
Upstream Galene's signalling protocol, unchanged. Documented in
galene-protocol.md
upstream. The frontend speaks it via protocol.js.
The browser joins with {type: "token", token} where the token is the
one the backend signed, and the group name is <prefix>/<slug> from the
join ticket.
Versioning¶
/api/v1/is v1. Breaking changes go to/v2/; minor additions (new endpoints, new fields with safe defaults) don't bump it.- The
/wsprotocol is upstream Galene's and versions with Galene. Beep pins a specific upstream ref, so it moves only when that ref does.