============
Architecture
============
.. admonition:: Who is this page for?
:class: note
Contributors and operators who need an accurate mental model of how a
request flows through GAME and why the layers are split the way they are.
Integrators can skip to :doc:`integrating`.
The layered design
==================
GAME is a layered application. Each layer has exactly one responsibility and
talks only to the layer directly beneath it::
HTTP request
│
▼
┌─────────────┐ FastAPI routers in app/api/v1/endpoints/*
│ Endpoint │ • validation (Pydantic schemas)
│ layer │ • authentication & scoping
└─────────────┘ • request/response shaping, audit logging
│
▼
┌─────────────┐ app/services/*
│ Service │ • business logic, transactional behavior
│ layer │ • orchestration across repositories
└─────────────┘ • domain rules and invariants
│
├───────────────► ┌───────────────┐ app/engine/*
│ │ Strategy │ • scoring strategies
│ │ engine │ • built-ins + sandboxed DSL
│ └───────────────┘
▼
┌─────────────┐ app/repository/*
│ Repository │ • persistence abstraction
│ layer │ • SQLAlchemy 2.0 async queries
└─────────────┘ • no business logic
│
▼
┌─────────────┐ PostgreSQL (async, via asyncpg)
│ Database │
└─────────────┘
The same boundaries appear in the directory tree:
.. list-table::
:header-rows: 1
:widths: 26 30 44
* - Layer
- Package
- Responsibility
* - Endpoint
- ``app/api/v1/endpoints/``
- HTTP interface: routing, request validation, authentication,
per-request audit logging, mapping domain errors to HTTP responses.
* - Service
- ``app/services/``
- Business logic and orchestration. The *only* place transactions and
multi-repository workflows live.
* - Strategy engine
- ``app/engine/``
- Adaptive/deterministic scoring. Pluggable strategies selected by
``strategyId``; includes the DSL interpreter and validator.
* - Repository
- ``app/repository/``
- Thin persistence layer over SQLAlchemy ``AsyncSession``. CRUD and
queries only - no domain decisions.
* - Model
- ``app/model/``
- SQLModel/SQLAlchemy table definitions (the database schema).
* - Schema
- ``app/schema/``
- Pydantic request/response contracts (the wire format).
Why this split matters:
* **Pluggable strategies** - scoring logic is isolated in ``app/engine`` and
selected by id, so new strategies drop in without touching endpoints or
repositories.
* **Deterministic services** - business rules live in one layer, making them
unit-testable without HTTP or a real database.
* **Reproducible behavior** - persistence is abstracted, so the same service
logic runs against PostgreSQL in production and SQLite in tests.
The life of a scoring request
=============================
Tracing ``POST /api/v1/games/{gameId}/tasks/{externalTaskId}/points`` end to
end (see ``app/api/v1/endpoints/games_points.py``):
#. **Middleware** - the request passes through the outer middleware stack
(CORS → unhandled-error catcher; see below) and, if metrics are enabled,
the Prometheus instrumentator.
#. **Auth dependency** - ``Depends(auth_api_key_or_oauth2)`` resolves an
``X-API-Key`` header *or* an OAuth2 bearer token. Failure short-circuits
with ``401``/``403`` before any business logic runs.
#. **Audit context** - ``Depends(audit_log("game"))`` builds an
``AuditLogger`` bound to the authenticated principal (API key, OAuth user
id, admin flag). Every meaningful step is logged with a correlation id.
#. **Validation** - the JSON body is parsed into a Pydantic schema
(``AsignPointsToExternalUserId``); malformed input yields ``422`` before
the handler body executes.
#. **Abuse prevention** - ``AbusePreventionService`` enforces per-API-key,
per-IP, and per-user rate limits and daily quotas; over-limit yields
``429`` (see :doc:`security`).
#. **Service** - ``UserPointsService.assign_points_to_user`` orchestrates the
work: resolve/lazily-create the user, load the task's effective strategy,
run scoring, persist ``UserPoints``, and move the wallet - within a
transaction.
#. **Strategy engine** - the resolved strategy computes ``points`` and a
``caseName``. For custom strategies this runs the sandboxed DSL
interpreter (:doc:`dsl-engine`).
#. **Repository** - persistence happens through repositories over an async
session; idempotency keys prevent double-awards on retry.
#. **Response** - the service returns a domain object serialized by the
response model. On any exception the endpoint maps it to a structured
error (preserving the correlation id) and records an audit error.
The middleware stack (and a subtle ordering bug it fixes)
=========================================================
Middleware is registered in ``app/main.py``. FastAPI's ``add_middleware``
*prepends*, so the **last** registered middleware is the **outermost**. GAME
relies on this on purpose::
add_middleware(CatchUnhandledErrorsMiddleware) # registered first
add_middleware(CORSMiddleware) # registered second → outermost
The CORS layer must wrap the error-catcher so that when an unhandled
exception is rendered as a ``500`` *from inside* the stack, the response
still passes back through CORS and receives its ``Access-Control-Allow-*``
headers. Without this ordering the browser blocks the error response and the
dashboard shows a bare *"Network Error"* with no clue that the real cause was
a backend ``500``.
.. admonition:: Operator tip
:class: tip
A dashboard "Network Error" with no HTTP status almost always means a
backend ``500`` whose body the browser dropped. Check the API logs
(``docker logs GAME_API_DEV``) for the real traceback rather than trusting
the browser message.
Dependency injection
====================
Wiring is centralized in a single `dependency-injector
`_ container,
``app/core/container.py``:
* ``db`` is a **Singleton** - one async engine/connection pool per process.
* Repositories and services are **Factories** - a fresh instance per
resolution, each handed the dependencies it declares.
* A few components are deliberately **Singletons** because they carry
process-wide state: the DSL execution-log observer (its sampling RNG and
background queue), the API-key cache backend, and the rate-limit counter
backend.
Endpoints never construct services directly. They declare what they need with
``Depends(Provide[Container.])`` and the container supplies a fully
wired instance. The ``wiring_config`` in the container lists exactly which
endpoint modules participate in injection.
This is what makes the layers swappable: tests override providers (for
example pointing ``db`` at SQLite) without changing a line of endpoint or
service code.
Async & persistence model
=========================
* The application is **async end to end** - FastAPI handlers, services, and
repositories are coroutines; persistence uses SQLAlchemy 2.0's
``AsyncSession`` over ``asyncpg``.
* ``BaseRepository`` (``app/repository/base_repository.py``) provides the
common CRUD vocabulary - ``read_by_id``, ``read_by_options`` (paginated +
filtered + ordered), ``read_by_column(s)``, ``create``, ``update``,
``whole_update``, ``delete_by_id`` - each opening a session from the
injected ``session_factory``.
* ``create`` supports an **externally managed session** (``auto_commit=False``)
so a service can compose several writes into one transaction; the default
path commits per call.
* Connection pooling is configured on the singleton engine (pool size,
overflow, pre-ping, recycle) and tuned via environment variables - see
:doc:`configuration`.
Because the app holds no per-request server state beyond the database, it is
**stateless and horizontally scalable**: run N replicas behind a load
balancer and let PostgreSQL (and optionally Redis) be the shared state.
Cross-cutting concerns
======================
These are not layers but slices that cut across the stack; each has its own
page:
* **Authentication & authorization** → :doc:`authentication`, :doc:`security`
* **Rate limiting & abuse prevention** → :doc:`security`
* **Observability** (metrics, logs, Sentry, execution traces) →
:doc:`observability`
* **Configuration** (every environment variable) → :doc:`configuration`
Where to go next
================
* :doc:`domain-model` - the entities these layers move around.
* :doc:`dsl-engine` - the strategy engine in depth.
* :doc:`codebase` - the auto-generated API reference for every module.