Arena Card signs a player in with their in-game id and zone, verifies them with a code, and renders one page of ranked statistics, battle highlights, most-played heroes and recent matches. Every call to the game API happens on the server: the browser only ever talks to this application.
One page, one job: show a player what their account looks like. Sign-in takes an in-game id and zone, sends a verification code, and exchanges it for a session. After that the page renders a profile snapshot, performance statistics, battle highlights, most-played heroes, and paginated recent matches with a detail view per match.
It is the counterpart to Arena Academy — the same upstream API, the opposite architecture. Academy holds a session in the browser and calls the API from there; Arena Card holds the session server-side and sends the browser finished HTML, with one same-origin request behind the match detail view.
There is no session store, because nothing worth keeping between requests is too large for a cookie. The session middleware is written directly against the ASGI interface: the payload is JSON, base64url-encoded and signed with HMAC-SHA256 over the raw bytes, compared in constant time, with a one-day maximum age and HttpOnly, SameSite=Lax and Secure outside debug.
Signed is not encrypted, and the design respects the distinction. The cookie is tamper-evident rather than confidential, so it carries the caller’s own credentials and nothing about anyone else. Page scripts never touch it; the bearer token is attached server-side on every upstream call.
Flash messages, form repopulation after a failed submit, and the last raw payload for the debug view all ride the same mechanism, which keeps the server stateless enough to run on a per-request platform.
Two failure modes shaped the settings layer, and both were real.
A deployment console that declares a variable and leaves the value blank passes through an empty string. For a boolean or an integer setting, that is not a missing value but an invalid one, and validation failed at import — before the application existed to report why. Blank values for non-string settings are now dropped so the default applies.
The other was the rebrand. Every API host the app had been configured against was retired at once, and sign-in broke for exactly that reason: stale values in a console nobody had revisited. Retired hosts are now mapped forward at the point the setting is read, with a fallback host behind the primary, so a value nobody has updated still resolves to something that answers.