Table of contents
- API Reference
- Base URL
- Authentication
- Errors
- Service endpoints
- Account object
- Registration and login
- Profile
- GET /api/v1/accounts/me
- PATCH /api/v1/accounts/me
- POST /api/v1/accounts/me/resend-verification
- POST /api/v1/accounts/verify-email
- GET /verify-email?token=...
- DELETE /api/v1/accounts/me
- Password reset
- Leaderboard
- Chess
- Release proxy
- Management panel
- Rate limits
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
API Reference
Base URL
https://account.example.com
All endpoints are under /api/v1/ unless noted. Requests and responses are JSON. The JSON body limit is 16 KB.
Authentication
Protected routes require:
Authorization: Bearer <jwt>
Tokens are HS256 JWTs with the issuer universal-gaming and audience universal-gaming-app. They contain:
| Claim | Meaning |
|---|---|
sub |
Account UUID |
username |
Username at issue time |
ver |
Session version (auth_version) |
exp |
Expiry |
A password reset bumps auth_version, so every older token returns 401 Session expired. Sign in again.
Errors
Errors use an error string:
{ "error": "Invalid username, e-mail or password." }
| Status | Meaning |
|---|---|
400 |
Invalid input |
401 |
Missing/invalid session, wrong password |
403 |
Feature disabled, registration closed |
404 |
Unknown route, account, match or release |
409 |
Conflict: duplicate username/e-mail, stale match, wrong turn |
413 |
Request body too large |
429 |
Rate limit exceeded |
502 |
Upstream release feed or e-mail delivery failed |
503 |
SMTP not configured |
Service endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/ |
No | Service name and version |
GET |
/healthz |
No | Health plus database kind |
GET |
/api/v1/meta |
No | Feature flags and version |
Example:
curl https://account.example.com/healthz
{
"status": "ok",
"service": "universal-gaming-accounts",
"version": "1.4.0",
"database": "postgres"
}
/api/v1/meta:
{
"name": "Universal Gaming Accounts",
"version": "1.4.0",
"registrationEnabled": true,
"updateProxyEnabled": true,
"leaderboardEnabled": true,
"chessEnabled": true,
"emailConfirmationEnabled": true,
"emailDeliveryConfigured": true,
"managementEnabled": true
}
Account object
{
"id": "9f1c2f7e-3d1a-4e7a-9d55-2a0b6c8e4f10",
"username": "player",
"email": "player@example.com",
"emailVerified": true,
"createdAt": "2026-01-01T12:00:00.000Z",
"updatedAt": "2026-01-02T12:00:00.000Z"
}
email is null when none is assigned. The UUID never changes when the username changes.
Registration and login
POST /api/v1/accounts
Creates an account. Rate limited with the auth limiter.
{
"username": "player",
"password": "correct horse battery staple",
"email": "player@example.com"
}
- Username: 3–32 characters, letters, numbers,
.,_,-. - Password: 8–128 characters.
- E-mail: optional, at most 254 characters.
- Returns 201 with
{ token, user }. - Returns 403 when registration is disabled.
- Returns 409 for a duplicate username or e-mail.
If an e-mail is supplied, a confirmation mail is sent when SMTP and e-mail confirmation are enabled. The account is usable immediately either way.
POST /api/v1/auth/login
{
"identifier": "player",
"password": "correct horse battery staple"
}
identifier may be a username or e-mail. username is accepted as a legacy alias. Returns { token, user }.
Failed logins log only the client IP. The response is always:
{ "error": "Invalid username, e-mail or password." }
GET /api/v1/accounts/check-username?username=player
Public availability check. Rate limited.
{ "username": "player", "available": false }
Profile
GET /api/v1/accounts/me
Returns { user } for the current token.
PATCH /api/v1/accounts/me
Rename and/or change e-mail:
{
"username": "new-name",
"email": "new@example.com"
}
At least one field is required. Changing the e-mail clears the verification state, clears any pending password-reset token and sends a new confirmation mail when enabled. Changing to null removes the address.
Returns { user }.
POST /api/v1/accounts/me/resend-verification
Resends the confirmation mail. Requires an assigned, unverified address, e-mail confirmation enabled and SMTP configured.
| Status | Meaning |
|---|---|
200 |
{ "sent": true } |
400 |
No e-mail assigned |
403 |
Confirmation disabled |
409 |
Already verified |
502 |
SMTP delivery failed |
503 |
SMTP not configured |
POST /api/v1/accounts/verify-email
{ "token": "..." }
Returns { user } with emailVerified: true. Tokens are single-use and expire after 48 hours.
GET /verify-email?token=...
Browser-facing confirmation page. Returns HTML, not JSON. Marks the address verified and tells the user to return to the app.
DELETE /api/v1/accounts/me
Requires password confirmation:
{ "password": "correct horse battery staple" }
Deletes the account, leaderboard rows and chess matches through database cascades. Returns 204.
Password reset
POST /api/v1/auth/forgot-password
{ "email": "player@example.com" }
- Requires SMTP; otherwise 503.
- Only accounts with an assigned and confirmed address receive mail.
- Always returns 202
{ "sent": true }, whether or not the address exists, to prevent account enumeration. - Tokens are 32 random bytes, stored only as SHA-256 hashes.
- Link format:
${APP_PUBLIC_URL}/account?reset=<token>. - Default lifetime: 30 minutes (
PASSWORD_RESET_TTL_MINUTES).
POST /api/v1/auth/reset-password
{
"token": "...",
"password": "new password"
}
Returns { "reset": true }. The token is cleared, all existing sessions are invalidated, and the user must sign in again.
Leaderboard
Ranked games:
sudoku, killer-sudoku, mahjong, minesweeper, klondike, classic-solitaire, spider, freecell, pyramid, tripeaks.
Chess is not ranked.
GET /api/v1/leaderboards
Query parameters:
| Name | Default | Description |
|---|---|---|
gameId |
all | Optional game filter |
limit |
5 |
Entries per mode, 1–50 |
GET /api/v1/leaderboards/:gameId
Same payload, scoped to one game.
{
"updatedAt": "2026-01-02T12:00:00.000Z",
"limit": 5,
"games": [
{
"gameId": "sudoku",
"modes": [
{
"mode": "Expert",
"entries": [
{
"rank": 1,
"username": "player",
"seconds": 412,
"moves": 87,
"detail": "No hints",
"achievedAt": "2026-01-02T11:00:00.000Z"
}
]
}
]
}
]
}
Ranking order: seconds, then moves, then achievedAt, then user_id.
POST /api/v1/leaderboards/:gameId/scores
Requires authentication. Rate limited to 30 submissions per minute.
{
"mode": "Expert",
"seconds": 412,
"moves": 87,
"detail": "No hints"
}
mode: 1–32 characters, letters, numbers, spaces,.,_,+,-.seconds: 0 to 30 days.moves: 0 to 1,000,000.detail: optional, at most 120 characters.
Only one score is stored per user, game and mode. A worse score does not replace the existing best.
{
"improved": true,
"rank": 1,
"score": {
"gameId": "sudoku",
"mode": "Expert",
"username": "player",
"seconds": 412,
"moves": 87,
"detail": "No hints",
"achievedAt": "2026-01-02T11:00:00.000Z"
}
}
Returns 403 when LEADERBOARD_ENABLED=false.
Chess
All chess routes require authentication and return 403 when CHESS_ENABLED=false. Rate limit: 120 requests per minute.
The server replays the complete move history with chess.js. The client is never trusted for legality, turn order or results.
Match object
{
"id": "uuid",
"status": "active",
"moves": ["e2e4", "e7e5"],
"turn": "w",
"result": null,
"version": 2,
"createdAt": "2026-01-01T12:00:00.000Z",
"updatedAt": "2026-01-01T12:01:00.000Z",
"finishedAt": null,
"lastMoveAt": "2026-01-01T12:01:00.000Z",
"white": { "username": "alice" },
"black": { "username": "bob" },
"you": "white",
"yourTurn": true
}
Statuses: pending, active, declined, cancelled, finished.
moves are UCI strings (e2e4, e7e8q). result is white, black or draw.
Routes
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/chess/matches |
Up to 30 matches involving the user |
GET |
/api/v1/chess/matches/:id |
One match |
POST |
/api/v1/chess/challenges |
Challenge a username |
POST |
/api/v1/chess/challenges/:id/accept |
Accept as the challenged player |
POST |
/api/v1/chess/challenges/:id/decline |
Decline as the challenged player |
POST |
/api/v1/chess/challenges/:id/cancel |
Cancel as the challenger |
POST |
/api/v1/chess/matches/:id/moves |
Submit a move |
POST |
/api/v1/chess/matches/:id/resign |
Resign |
Create a challenge:
{ "opponentUsername": "bob" }
The challenger plays White. Returns 201 with { match }.
Submit a move:
{
"from": "e2",
"to": "e4",
"promotion": "q"
}
promotion is optional and must be q, r, b or n.
{
"match": { "...": "..." },
"move": {
"uci": "e2e4",
"san": "e4",
"check": false,
"checkmate": false,
"draw": false
}
}
Move conflicts return 409:
- Not your turn.
- Match not active.
- The match changed while saving (optimistic
versioncheck).
Resigning returns { match } with status: "finished" and the opponent as the winner.
Release proxy
The app uses these routes to avoid browser CORS restrictions. The backend caches the upstream feed and sanitizes every field.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/app/releases |
Up to 20 releases, newest first |
GET |
/api/v1/app/latest |
Newest non-draft, non-prerelease release |
A release contains:
{
"tag_name": "v2.9.0",
"name": "Universal Gaming 2.9.0",
"body": "Release notes…",
"html_url": "https://git.emilyh.de/Emily/UniversalGamingApp/releases/tag/v2.9.0",
"published_at": "2026-01-01T12:00:00Z",
"created_at": "2026-01-01T11:00:00Z",
"draft": false,
"prerelease": false,
"assets": [
{
"name": "UniversalGaming-2.9.0-android.apk",
"browser_download_url": "https://…",
"size": 59600000
}
]
}
Returns 404 when UPDATE_ENABLED=false and 502 when the upstream feed fails.
Management panel
GET /management returns a read-only HTML dashboard with counts and recent activity. It uses HTTP Basic authentication.
- Disabled when
MANAGEMENT_PASSWORDis empty. - Username:
MANAGEMENT_USER(defaultadmin). Cache-Control: no-store.
It is an operator convenience, not a public API. Restrict it at the reverse proxy where possible.
Rate limits
| Limiter | Window | Limit | Applies to |
|---|---|---|---|
| Auth | 15 min | 30 | Registration, login, reset-password |
| Username check | 1 min | 60 | Availability checks |
| 15 min | 8 | Forgot-password, resend verification, verify-email | |
| Chess | 1 min | 120 | All chess routes |
| Leaderboard read | 1 min | 120 | Leaderboard GETs |
| Leaderboard write | 1 min | 30 | Score submissions |
Rate limits use the client IP derived from the trusted-proxy configuration.
Universal Gaming Accounts
- Home
- Deployment
- Configuration
- API Reference
- Database and Migrations
- Security
- Operations
- Development and Testing
- Troubleshooting
Repositories
The accounts backend is optional. Universal Gaming remains fully playable offline without it.