Gamers Lab Docs

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 200 with playerId, or 404 if not found.

Create player (optional) POST /api/player-auth/players

  • Creates a new player account and performs an automatic login.
  • Returns 201 with login payload (access token, refresh token, session ID).
  • Returns 409 if 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:

FieldRequiredMax LengthNotes
platformYesEnum value from supported platforms list
clientVersionNo32 charse.g. "1.0.0"
clientBuildNo64 charsBuild identifier
metadataNoCustom key-value data

deviceInfo:

FieldRequiredMax LengthNotes
deviceFingerprintYes16–256 charsStable hash of hardware identifiers
hardwareModelNo128 charse.g. "PlayStation 5 Digital Edition"
osVersionNo64 charse.g. "Windows 11 23H2"
metadataNoCustom 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.
  • sessionId is 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

CodeMeaning
400Invalid payload (missing required fields, malformed data)
401Invalid game key or provider token
404Player, match, or resource not found
409Conflict (player already exists, match already ended, duplicate result, idempotency key conflict)
410Login session expired or ended
422Provider disabled, definition missing (auto-create off), or no records accepted
429Rate 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.

OperationRequired
Match createYes
Match joinYes
Match leaveYes
Match endYes
Match resultsYes
Event records (match-scoped)Yes — per record in the batch
Player event recordsYes — 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.

  1. The service normalises the idempotency key and computes a SHA-256 hash of the full request payload.

  2. A Redis key is checked: GamersLabAPI:gamewrite:idempotency:{tenantId}:{endpoint}:{idempotencyKey}

    Endpoint values: match:create, match:join, match:leave, match:end, match:results

    DecisionMeaningResponse
    NewKey not seen beforeProceed with write
    AlreadyProcessedSame key + same payload, previously completedReturn cached response; alreadyProcessed: true
    ConflictSame key + different payload409 Conflict
    InProgressSame key currently being processed429 / transient error
  3. Redis entry status is set to processing with a 1-hour TTL. The match operation runs.

  4. On success, the Redis entry is updated: status becomes completed and the response JSON is stored. Subsequent retries replay this cached response without touching the database.

  5. 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

MatchesEvents
Redis in-flight detectionYesNo
Response caching (replay)Yes — full response stored in RedisNo
Conflict detectionService layer — same key + different payload → 409Not applicable; duplicate = DO NOTHING
alreadyProcessed flagYesNo — caller inspects per-record result lists
Batch supportNo — one operation per requestYes — up to 10,000 records per request
Deduplication window1 hour (Redis TTL)Permanent (database unique constraint)

The alreadyProcessed Flag

All match write responses include alreadyProcessed: bool:

  • false — the operation was processed fresh
  • true — 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

ScenarioHTTPDetail
Missing idempotency key400IdempotencyKey is required
Invalid characters or too long400validation message
Same key + different payload (match)409IdempotencyKey already used with a different payload
Same key currently in-flight (match)409IdempotencyKey is already being processed
Duplicate event recordRecord 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

  1. Take a pre-test snapshot of match table sizes using Shared/Database/measure_match_storage.sql.
  2. Run a controlled match simulation with a known number of matches.
  3. Take a post-test snapshot of the same tables.
  4. Query game key usage logs for write bytes in the test window.
  5. 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:

TableRows per matchWhat it stores
game_app_match_sessions1Match metadata (start, mode, map, etc.)
game_app_match_ends1Match completion record
game_app_match_players1 per playerPlayer participation records
game_app_player_results1 per playerPlayer scores and outcomes
game_app_match_player_eventsvariesIn-match events (joins, leaves, kills, etc.)

Reference Benchmark

Based on a 10-match test run (8 players per match, ~16 events per match):

MetricPer 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.

On this page