Skip to content

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}/join with {"display_name": "Alice"}.
  • Local-password users — argon2 hashes in the users table. /auth/login returns 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 doc
  • notes/v2/data-model.md — domain tables
  • notes/v2/plugins.md — plugin architecture
  • backend/README.md — operational notes
  • local-notes/playbooks/litestar-dishka/ — stack playbooks