Found by running the stack for the first time:
- compose build context pointed outside the repo. Relative paths in
.devcontainer/compose.override.yaml resolve against the project
directory (the repo root), not the file's own directory, so "context: .."
escaped the repository and the image could not build at all.
- The devcontainers/python base image ships a yarn apt source whose
signing key has rotated, failing apt-get update and the whole build.
Drop that source list; we don't use yarn.
- pytest-asyncio ran fixtures on a session-scoped loop while tests ran on
per-function loops, so asyncmy raised "Future attached to a different
loop" on every database-backed test. aiosqlite masked this; MySQL does
not. Fixture loop scope now matches the test loop scope.
- MySQL DATETIME stores no offset, so timestamps serialized bare and left
clients guessing. Connections are pinned to UTC, so DeviceRead now
attaches that offset explicitly, with a test covering it.
Verified end to end against real MySQL 8.4 and Keycloak 26.7: cold start
from destroyed volumes, uv sync --frozen, migrations, 41 tests, ruff,
mypy --strict, and the live authorization matrix driven by real tokens.
Co-Authored-By: Claude Opus 5 <[email protected]>
Sets up the project skeleton:
- FastAPI app factory with lifespan, request-id middleware, and RFC 9457
problem+json error handlers
- Async SQLAlchemy 2.0 over MySQL (asyncmy), with a constraint naming
convention in place before the first migration and async Alembic
- Keycloak as a pure resource server: OIDC discovery, cached JWKS with
rotation-aware refresh, and require_roles dependencies
- Devcontainer running MySQL 8.4 and Keycloak 26.7 as compose siblings,
with the realm (clients, roles, test users) imported on first boot
- Test suite covering the endpoints plus the token validator itself,
exercised against a locally generated RSA keypair
- uv packaging, ruff, mypy --strict, pre-commit, Gitea CI, prod Dockerfile
Two Keycloak-in-containers traps are handled explicitly and documented in
the README: the issuer/internal-URL split (the browser sees localhost:8080,
the API sees keycloak:8080) and the audience mapper that stops Keycloak
issuing tokens with aud=account.
The devices resource is a placeholder proving the routing -> auth -> ORM ->
migration path end to end; replace it with the real domain.
Co-Authored-By: Claude Opus 5 <[email protected]>