2.1 General
Section 2 — Games
2.1 General
2.1.1 Game Life Cycle
Quick reference for game developers integrating Gamers Lab into their games. Covers player authentication, match lifecycle, event tracking, player profiles, and logout.
Headers
All game write and player auth endpoints (unless noted) require a game auth key:
X-Game-Key: gk_live_...Supported Auth Providers
- Steam
- Epic Games
- Sequence (wallet)
- EVM Wallet (challenge-based)
- Email + Password
- Email One-Time Code
- Mock (testing only)
Supported Platforms
NintendoSwitch, NintendoSwitchLite, NintendoSwitchOLED, PlayStation4,
PlayStation4Pro, PlayStation5, PlayStation5Pro, PlayStationVR, PlayStationVR2,
XboxOne, XboxOneS, XboxOneX, XboxSeriesS, XboxSeriesX, PC_Windows, PC_Mac,
PC_Linux, PC_SteamDeck, Mobile_iOS, Mobile_Android, MetaQuest2, MetaQuest3,
MetaQuestPro, ValveIndex, HTCVive, Cloud_GeForceNow, Cloud_XboxCloud,
Cloud_Luna, Other
Unknown also exists as the server fallback/default value. See §2.2 for the full
GamePlatform enum.
Player Authentication
Check if player exists (optional)
POST /api/player-auth/players/exists
- Body:
provider+providerUserId. - Returns
200withplayerId, or404if not found.
Create player (optional)
POST /api/player-auth/players
- Creates a new player account and performs an automatic login.
- Returns
201with login payload (access token, refresh token, session ID). - Returns
409if the player already exists.
Login
POST /api/player-auth/login
- Authenticates a player with a provider token.
createAccountIfMissing(boolean) — if true, auto-creates the player on first login.
Request example:
{
"provider": "Steam",
"token": "steam-session-ticket",
"createAccountIfMissing": true,
"clientInfo": {
"platform": "PC_Windows",
"clientVersion": "1.0.0"
},
"deviceInfo": {
"deviceFingerprint": "a1b2c3d4e5f6g7h8"
}
}Response example:
{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"tokenType": "Bearer",
"expiresIn": 7200,
"playerId": "guid",
"tenantId": "guid",
"isNewPlayer": false,
"sessionId": "guid"
}Client and Device Info Fields
clientInfo captures session/client telemetry. deviceInfo captures durable fingerprint-based
device tracking data. Both objects are optional.
clientInfo:
| Field | Required | Max Length | Notes |
|---|---|---|---|
platform | Yes | — | Enum value from supported platforms list |
clientVersion | No | 32 chars | e.g. "1.0.0" |
clientBuild | No | 64 chars | Build identifier |
metadata | No | — | Custom key-value data |
deviceInfo:
| Field | Required | Max Length | Notes |
|---|---|---|---|
deviceFingerprint | Yes | 16–256 chars | Stable hash of hardware identifiers |
hardwareModel | No | 128 chars | e.g. "PlayStation 5 Digital Edition" |
osVersion | No | 64 chars | e.g. "Windows 11 23H2" |
metadata | No | — | Custom key-value data |
Refresh Token
POST /api/player-auth/refresh
- Rotates the refresh token — the old token is immediately revoked (no grace period).
- Returns a new access token and refresh token.
Token Lifetimes
- Access token: 2 hours
- Player refresh token: 14 days
Player Email Auth (No X-Game-Key)
Email auth lets players authenticate without a platform provider. These endpoints do not
require X-Game-Key unless noted.
Link email to existing player (JWT required)
POST /api/player-auth/email/link
{ "email": "player@example.com", "password": "StrongPass123!" }Returns 200 and sends a verification code to the email.
Verify email
POST /api/player-auth/email/verify
{ "email": "player@example.com", "code": "123456" }Verification codes expire in 24 hours and are single-use.
Login with email/password
POST /api/player-auth/email/login
{ "email": "player@example.com", "password": "StrongPass123!" }Email signup (requires X-Game-Key)
POST /api/player-auth/email/signup
- Restricted to development game keys.
Tenant-scoped email login (requires X-Game-Key)
POST /api/player-auth/email/tenant-login
- Restricted to development game keys.
One-time code flow
POST /api/player-auth/email/otc (requires X-Game-Key) — sends a one-time login code, binds it to that tenant, and requires a development game key (expires in 10 minutes)
POST /api/player-auth/otc/exchange (requires X-Game-Key) — exchanges for tokens for the same tenant and also requires a development game key
Forgot / reset password
POST /api/player-auth/email/forgot-password — sends a reset link (token expires in 1 hour)
POST /api/player-auth/email/reset-password — resets the password using the token
Match Lifecycle Summary
Match write endpoints require X-Game-Key and a player bearer token, except
POST /api/game/matches/players/resolve, which uses only X-Game-Key. Typical flow:
create → join → events → end → results → leave.
Match create and join require a fresh login session. The sessionId from login is
passed as loginSessionId. Sessions are valid for 2 hours since last activity and must
not be explicitly ended. Create returns per-player 410 validation errors for expired sessions;
join currently surfaces the same expired-session validation as 409 Conflict.
Other match/event operations (events, end, results, leave) do not enforce login session freshness, but they still require a valid game key, player bearer token, and correct match/player state.
Logout
POST /api/player-auth/logout
- Revokes the refresh token. The access token remains valid until its natural 2-hour expiry.
sessionIdis required. When present, the system automatically appends a match leave event for any active match player tied to that login session.- Required body fields:
refreshToken,sessionId. - Optional body fields:
playerId,tenantId,deviceId.
Error Semantics
| Code | Meaning |
|---|---|
400 | Invalid payload (missing required fields, malformed data) |
401 | Invalid game key or provider token |
404 | Player, match, or resource not found |
409 | Conflict (player already exists, match already ended, duplicate result, idempotency key conflict) |
410 | Login session expired or ended |
422 | Provider disabled, definition missing (auto-create off), or no records accepted |
429 | Rate limit or quota exceeded |
2.1.2 Idempotency
The Problem Idempotency Solves
Game servers operate over unreliable networks. A POST /api/game/matches/create request
might time out before the client receives the response — even though the server already
processed and committed it. Without idempotency, retrying would create a second match,
double-count results, or produce duplicate events.
Idempotency keys let the server detect and absorb retries safely. A retry with the same key and payload returns the original result with no second write.
What an Idempotency Key Is
An idempotency key is a unique string chosen by the caller and attached to a write request. It identifies the logical operation — not the HTTP request itself.
Key format:
- Allowed characters:
A–Z,a–z,0–9,.,_,:,- - Maximum length: 64 characters
- Whitespace is trimmed; empty or whitespace-only values are rejected
- Keys are tenant-scoped — the same string is independent across tenants
Caller responsibility: generate a stable key per logical operation. UUIDs are a natural fit. Do not reuse a key for a different operation — that is a conflict.
Required vs Optional
Idempotency keys are required on all game write operations. There is no opt-out.
| Operation | Required |
|---|---|
| Match create | Yes |
| Match join | Yes |
| Match leave | Yes |
| Match end | Yes |
| Match results | Yes |
| Event records (match-scoped) | Yes — per record in the batch |
| Player event records | Yes — per record in the batch |
Submitting without a key, or with an invalid key, returns 400 Bad Request before any write.
How It Works — Matches
Match writes use a two-tier approach: Redis for in-flight detection and response caching, with the database as the permanent record.
-
The service normalises the idempotency key and computes a SHA-256 hash of the full request payload.
-
A Redis key is checked:
GamersLabAPI:gamewrite:idempotency:{tenantId}:{endpoint}:{idempotencyKey}Endpoint values:
match:create,match:join,match:leave,match:end,match:resultsDecision Meaning Response NewKey not seen before Proceed with write AlreadyProcessedSame key + same payload, previously completed Return cached response; alreadyProcessed: trueConflictSame key + different payload 409 ConflictInProgressSame key currently being processed 429/ transient error -
Redis entry status is set to
processingwith a 1-hour TTL. The match operation runs. -
On success, the Redis entry is updated: status becomes
completedand the response JSON is stored. Subsequent retries replay this cached response without touching the database. -
On failure, the Redis entry is deleted so the caller can retry cleanly.
Database layer: All match ledger tables store idempotency_key per row. A unique index on
(tenant_id, row_hash) provides a second layer: a duplicate that bypasses Redis fails the
unique constraint.
How It Works — Events
Event ingestion operates at batch scale (up to 10,000 records per request). Redis is not used. Deduplication is handled entirely at the database layer.
Each record is validated individually — an invalid key produces a per-record error; other valid records still process. Valid records are bulk-inserted:
INSERT INTO game_app_event_records (...)
SELECT ...
ON CONFLICT (tenant_id, occurred_at, idempotency_key)
WHERE idempotency_key IS NOT NULL
DO NOTHING
RETURNING id;A duplicate (same tenant + occurred_at + idempotency_key) is silently skipped. Callers
identify accepted vs skipped records by comparing inserted IDs against submitted IDs.
Match vs Event Idempotency — Key Differences
| Matches | Events | |
|---|---|---|
| Redis in-flight detection | Yes | No |
| Response caching (replay) | Yes — full response stored in Redis | No |
| Conflict detection | Service layer — same key + different payload → 409 | Not applicable; duplicate = DO NOTHING |
alreadyProcessed flag | Yes | No — caller inspects per-record result lists |
| Batch support | No — one operation per request | Yes — up to 10,000 records per request |
| Deduplication window | 1 hour (Redis TTL) | Permanent (database unique constraint) |
The alreadyProcessed Flag
All match write responses include alreadyProcessed: bool:
false— the operation was processed freshtrue— the response is a replay of a previously completed operation; no new write occurred
Fail-Open Behaviour
The Redis idempotency cache is fail-open for matches: if Redis is unavailable, the
in-flight check is skipped and the request proceeds. Concurrent duplicates that reach the
database are caught by the row_hash unique constraint. Availability is prioritised over
strict duplicate prevention in the Redis-down case.
For events, there is no Redis dependency — fail-open does not apply.
Error Reference
| Scenario | HTTP | Detail |
|---|---|---|
| Missing idempotency key | 400 | IdempotencyKey is required |
| Invalid characters or too long | 400 | validation message |
| Same key + different payload (match) | 409 | IdempotencyKey already used with a different payload |
| Same key currently in-flight (match) | 409 | IdempotencyKey is already being processed |
| Duplicate event record | — | Record absent from accepted list; no error raised |
Known Limitations
Match idempotency key expiry (TODO): The Redis TTL is 1 hour. After expiry, a retry is
treated as New. There is no cross-check against the database to detect whether the key was
already committed. The canonical fix is to persist keys to a durable store alongside the match
record, or extend the TTL to the match retention window.
Event idempotency scope (TODO): The unique index is on (tenant_id, occurred_at, idempotency_key).
The same idempotency key submitted with different occurred_at values produces two distinct
rows — deduplication silently fails. Callers must use a stable occurred_at on retries.
Code References
- Key validation:
GamersLabRestAPI/Game/Helpers/GameWriteIdempotencyKeyHelper.cs - Redis state service:
GamersLabRestAPI/Game/Helpers/GameWriteIdempotencyService.cs - Match service:
GamersLabRestAPI/Game/Matches/Services/MatchService.cs - Event service:
GamersLabRestAPI/Game/Events/Services/EventService.cs - Match DTOs:
GamersLabRestAPI.Contracts/Game/Matches/DTOs/MatchDTOs.cs - Event DTOs:
GamersLabRestAPI.Contracts/Game/Events/DTOs/EventRecordDTOs.cs - Limits constants:
GamersLabRestAPI/Game/Constants/GameWriteLimits.cs - Match repository:
GamersLabRestAPI/Game/Matches/Repositories/NpgsqlMatchRepository.cs - Event repository:
GamersLabRestAPI/Game/Events/Repositories/NpgsqlEventRepository.cs - Core DB migrations:
GamersLabRestAPI/Shared/Database/200.sql
2.1.3 Data Visibility & Redaction Profiles
[NO DOC]
Match read responses can be scoped by a visibility type assigned to the API key's historical
data profile: None, Host, Self, Team, or Match. Field-level redaction rules strip
playerId, matchPlayerId, loginSessionId, teamId, and teamLabel from responses based
on the caller's visibility level. Configured per API key via HistoricalDataProfileConfig.
Applies to all match data read endpoints.
2.1.4 Match Storage Measurement
What It Is
A method for estimating how much storage each match consumes — both on-disk in the database and in ingest payload bytes. Useful for capacity planning, cost estimation, and understanding the per-match footprint of a tenant's game integration.
What Gets Measured
On-disk storage: The total database growth caused by match data, including table rows, indexes, and TOAST (large-value overflow storage). Measured by taking a size snapshot before and after a controlled test run.
Ingest payload bytes: The total HTTP request body size recorded during match ingestion.
The platform logs request_bytes per write request on every game auth key.
How to Run a Measurement
- Take a pre-test snapshot of match table sizes using
Shared/Database/measure_match_storage.sql. - Run a controlled match simulation with a known number of matches.
- Take a post-test snapshot of the same tables.
- Query game key usage logs for write bytes in the test window.
- Compute deltas and divide by match count.
Requirements:
- A test environment or isolated time window with no other traffic.
- The same seed data and match template for each run.
- Access to the game auth key used for ingestion (to filter usage logs by key).
What a Match Produces
A single match generates rows across five tables:
| Table | Rows per match | What it stores |
|---|---|---|
game_app_match_sessions | 1 | Match metadata (start, mode, map, etc.) |
game_app_match_ends | 1 | Match completion record |
game_app_match_players | 1 per player | Player participation records |
game_app_player_results | 1 per player | Player scores and outcomes |
game_app_match_player_events | varies | In-match events (joins, leaves, kills, etc.) |
Reference Benchmark
Based on a 10-match test run (8 players per match, ~16 events per match):
| Metric | Per match |
|---|---|
| On-disk growth (tables + indexes + TOAST) | ~34 KB |
| Ingest bytes (match events only) | ~36 KB |
| Ingest bytes (all match endpoints combined) | ~40 KB |
| Ingest bytes (all write endpoints) | ~42 KB |
Row counts for the 10-match run: 10 sessions, 10 ends, 80 players, 80 results, 160 player events.
Caveats
- Usage logs are per HTTP request, not per event row. Batched events reduce request count.
- On-disk growth includes indexes and TOAST, which can differ from raw payload bytes.
- Total database size deltas can include unrelated activity — always measure specific match tables rather than whole-database size.
- Isolate your test window. Background jobs or migrations during the test will skew results.