The reference backend (backend/)¶
Python Litestar app, serves as the reference implementation of
/api/v1/ and as a validator for the contract. Replaceable —
see alt-backend for a 150-line counter-example.
Stack¶
| Layer | Choice | Why |
|---|---|---|
| Web framework | Litestar | async, pydantic-native, OpenAPI out of the box |
| DI | Dishka | scope-aware, testable, no global containers |
| ORM | Advanced Alchemy | SQLAlchemy 2.x + Litestar wiring + repository pattern |
| Migrations | Alembic | standard |
| Plugins | Pluggy | project-native extension system |
| Config | pydantic-settings | typed, env-first |
| Logging | structlog + Litestar's LoggingConfig |
JSON logs, 4xx tracebacks suppressed |
| Package manager | uv |
fast, no pip |
| Tests | pytest + pytest-asyncio + FakeSFU | real SQLite, mocked SFU HTTP |
| Lint/format | ruff | one tool for both |
Module map¶
backend/src/app/
├── app.py ← create_app() factory, Litestar config, DI
├── settings.py ← pydantic-settings (env-driven)
├── db.py ← SQLAlchemy engine + session config
├── di.py ← Dishka providers
├── api/
│ ├── controllers.py ← /api/v1/ HTTP routes
│ └── dtos.py ← pydantic request/response shapes
├── domain/
│ ├── models.py ← SQLAlchemy models (User, Event, EventParticipant, Artifact)
│ └── services.py ← business logic (RoomService, JoinService, …)
├── persistence/ ← repositories (Advanced Alchemy)
├── sfu/
│ ├── token.py ← signs Galene join tokens (GaleneAuth)
│ ├── operator.py ← the per-room operator /ws connection
│ └── admin.py ← runtime view of the SFU, over the operator
├── sfu_events/
│ ├── bridge.py ← roster changes → dispatcher envelopes
│ └── dispatcher.py ← fans events out to the DB and Pluggy hooks
├── chat/ ← broker + fanout WebSocket
├── admin/ ← server-rendered admin UI (Jinja2 + htmx)
└── plugins/ ← Pluggy hookspecs and manager
Endpoints¶
The full list is the generated OpenAPI spec — hit
http://localhost:8000/schema/openapi.json while the backend is
running, or visit the Scalar UI at
http://localhost:8000/schema/scalar for rendered docs.
Highlights:
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/auth/login |
- | Local-password login, sets session cookie |
POST |
/api/v1/auth/logout |
session | Clears session |
GET |
/api/v1/me |
session | Current user (401 → guest) |
GET |
/api/v1/config |
- | Public runtime config |
POST |
/api/v1/rooms/ephemeral |
- | Create an ad-hoc event (no SFU call) |
GET |
/api/v1/events/by-slug/{slug} |
- | Resolve URL slug → event summary |
POST |
/api/v1/events/{id}/join |
session or guest | Mint a JWT ticket |
GET |
/api/v1/events/{id}/participants |
session | Attendees, from the operator connection |
POST |
/api/v1/events/{id}/chat |
chat token | Post a chat message |
WS |
/api/v1/events/{id}/chat-ws |
chat token | Chat fanout |
Auth model¶
- Guests — no login, just a display name. POST to
/events/{id}/joinwith{"display_name": "Alice"}. - Local-password users — argon2 hashes in the
userstable./auth/loginreturns a session token (JWT in an HttpOnly cookie). - OIDC — hookspecs exist, not wired in POC.
Guests and authenticated users both receive the same kind of SFU
ticket from /events/{id}/join. The token's claims differ
(authenticated users get their user id; guests get just a display
name), but the SFU doesn't care — it enforces only what the
backend signed.
Configuration¶
DATABASE_URL=sqlite+aiosqlite:///./beep.db
# Where the SFU lives, and how we sign for it
GALENE_BASE_URL=http://localhost:8443 # the host in a token's `aud`
GALENE_GROUP_PREFIX=meetings # the auto-subgroups parent
GALENE_AUTH_KEY=<base64url of 32 bytes> # must equal the group's authKeys `k`
GALENE_OPERATOR_WS_URL=ws://localhost:8443/ws # server-to-server, not the proxy
SFU_PUBLIC_WS_URL=ws://localhost:5173/ws # what the browser is told to use
JWT_SECRET_KEY=change-me # sessions and chat tokens
Two different WebSocket URLs
GALENE_OPERATOR_WS_URL is where the backend dials, so it points
at the SFU directly. SFU_PUBLIC_WS_URL is what the browser is
told, and in dev that's the Vite proxy so the upgrade is same-origin.
Running¶
# From repo root, as part of `make run`
make run-backend
# Standalone
cd backend
uv run uvicorn --factory app:create_app --reload --host 127.0.0.1 --port 8000
Testing¶
cd backend
uv run pytest -q # 55 pass + 3 live-skipped
uv run ruff check src tests
uv run alembic upgrade head
Integration tests use real SQLite (function-scoped in-memory
engine) and a FakeSFU httpx mock for the SFU client. See
notes/reviews/checklist.md
§8 for the testing doctrine.
Plugin system (scaffolded)¶
The Pluggy hookspecs live in backend/src/app/plugins/. The
reference "summarizer" plugin described in
notes/v2/plugins.md is not shipped
in the POC — it's planned as post-POC work once there's a concrete
use case.
What exists today: hookspecs (on_event_ended, on_participant_joined,
etc.), a manager, the events consumer that invokes them, and the
artifacts table for plugins to persist outputs into. Adding a
plugin is: subclass, register an entry point under galene.plugins,
uv pip install -e.
Further reading¶
notes/v2/backend.md— design docnotes/v2/data-model.md— domain tablesnotes/v2/plugins.md— plugin architecturebackend/README.md— operational noteslocal-notes/playbooks/litestar-dishka/— stack playbooks