kaya-oidc 0.0.6 can run several providers in one app (OIDCProviders), each with its own login route and namespaced session state. Adopt it: providers are configured in indexed, contiguous OIDC_<n>_* blocks (0-based) with OIDC_<n>_PROVIDER naming the provider in its login URL and session keys. The old unindexed OIDC_ISSUER/OIDC_CLIENT_ID/... names are no longer read; startup fails with a rename hint when they are set. With no OIDC variables at all, the single dev mock provider named "default" is used, as before. The platform now authenticates through OIDCProviders: auth.current_user returns the session's user (sessions stay single-identity, so at most one provider is logged in) and require_auth/route handlers use it, while the websocket path keeps scanning for any provider's user. New routes expose the configuration to the SPA and keep old links working: GET /api/auth/providers lists the login URLs, GET /auth/login redirects to the first provider and GET /auth/logout to whichever provider the session is logged in with. The lobby renders one login button per provider from that endpoint. Update the env examples, compose and k8s manifests to the indexed scheme, document the multiple-provider setup and the migration, and cover it with config, auth and route tests.
tavolo-platform
The game-independent half of tavolo: lobby, live play, match history and
leaderboards. Everything here works for any game that implements the
GameEngine contract; the package
itself ships no game.
Contents
engine.py— the platform↔game contract:GameEngine(ABC),GameSession(the platform-owned envelope with an opaquestateblob),Seat,Deadline(kind + due time + revalidation token) and theMatchResult/PlayerResultoutcome types.registry.py—GameRegistry: game id → engine lookup, single source of truth for which games exist.mixin.py—Platform(registry + store + scheduler + OIDC, the collaborators every endpoint needs) andPlatformMixin, the kaya mixin that registers all routes and the websocket endpoint on an app.routes/—health(GET /api/health),me(GET /api/me),games(lobby:GET /api/game-types,POST /api/games,POST /api/games/join,GET /api/games/{id}) andstats(GET /api/me/matches,GET /api/leaderboard,GET /api/me/ratings).ws.py— the live-play websocket (/ws/games/{id}): connection lifecycle, the message envelope and the publish/subscribe fan-out. Game-specific actions are dispatched to the session's engine.store.py— live-session persistence:RedisGameStore(production) andInMemoryGameStore(tests/dev) over a JSON envelope plus the engine's opaque state blob, with per-game locks, change signals and the shared deadline queue.deadlines.py—DeadlineScheduler: enqueue the deadline an engine declares after each mutation; a per-loop consumer fires due entries throughengine.fire_deadlineunder the per-game lock. Engines revalidate the token, so stale or duplicate deliveries are harmless.models.py—Match(game_type, timestamps, game-specific JSONresult),MatchPlayer(seat, team, won, points, Elo delta, JSONdetails) andPlayerRating(Elo per(user_sub, game_type)).stats.py—save_match_result(engineMatchResult→ Postgres, transactionally, with Elo) andapply_elo.elo.py— chess-style Elo math generalized to two-team matches.backfill_elo.py— rebuild every rating from the match history:python -m tavolo.platform.backfill_elo --database-url postgres://….auth.py,http.py,pagination.py,openapi.py,tortoise_mixin.py— OIDC helpers, JSON helpers, keyset pagination, shared OpenAPI fragments, the TortoiseORM lifecycle mixin.
Adding a game
Implement GameEngine (see engine.py for the full contract), register
it in a GameRegistry, and mount PlatformMixin(Platform(...)) on a
KayaApp — see the composition root in tavolo.app. The canonical
example is
tavolo-scopone.
Development (from server/)
.venv/bin/python -m unittest discover -s packages/tavolo-platform/tests
.venv/bin/python -m mypy -p tavolo.platform
Tests run fully in-process against a DummyEngine (a two-player toy game
in tests/helpers.py): in-memory sqlite, in-memory stores, a patched
OIDC user and httpx / httpx-ws ASGI transports. No test in this
package may import a real game.