Skip to content

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. /me returns 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 /ws protocol is upstream Galene's and versions with Galene. Beep pins a specific upstream ref, so it moves only when that ref does.