4.2 Game Auth Keys (X-Game-Key)
Section 4 — API & Game Key Management
4.2 Game Auth Keys (X-Game-Key)
Game Auth Keys identify a game build or game server that is allowed to send data into Gamers Lab. They are passed in the X-Game-Key request header.
They are used by:
- game servers
- game clients
- backend ingestion pipelines for games
They are never used by dashboards or analytics.
What they can do:
- Authenticate a game to Gamers Lab
- Create login events
- Create match sessions
- Add players to matches
- Submit match-scoped event records
- Submit player-scoped event records
- Submit match results
Trust level: High. If a Game Auth Key is compromised, attackers can inject fake matches and poison analytics. These keys must be handled like secrets.
Security characteristics:
- Stored as
key_hash - Scoped implicitly by purpose (write-only)
- Bound to
tenant_idand environment (development,production) - Rate limited (Redis Lua-based atomic minute/hour counters)
- Subject to quota enforcement (approximate hard-stop on the write path)
- Carries billing context per request (billed workspace, quota bucket, billable units)
- Optional origin restrictions (web clients)
Write key rate limits (per key):
- Free plan default: 10,000 requests/minute, 46,000 requests/hour.
- Per-tenant overrides available; overrides are clamped to a minimum of 1.
- Plan changes propagate to existing keys.
Rate limit behavior:
- Write rate limiting is fail-open — if Redis is unavailable, requests are allowed.
- Exceeding limits returns HTTP 429 with
Retry-AfterandX-RateLimit-Resetheaders.
Key count limits:
- Default max write keys (game auth keys): 3 per tenant (plan-driven).
Key format: Development keys use the gk_dev_ prefix. Production keys use the gk_live_ prefix.
Key lifecycle: create, list, get, get limits, update, rotate, revoke, and delete. Operations require a SaaS JWT with owner or admin role on the tenant.
| Operation | Effect |
|---|---|
| Create | Generates a new key; plaintext returned once; hash stored |
| List | Returns all game auth keys for the tenant; plaintext never included |
| Get | Returns a single game auth key by ID; prefix and metadata only |
| Get limits | Returns persisted per-key minute/hour limits plus warning and critical thresholds |
| Update | Updates mutable fields (name, description, environment, allowedOrigins, isActive) |
| Rotate | Generates a new secret in-place on the same key ID; old secret immediately invalidated; new plaintext returned once |
| Revoke | Soft-deactivates the key and records revoked metadata. Re-enabling through update clears revoked metadata. |
| Delete | Hard-deletes the key row from the database |
4.2.1 Game Key Analytics
Every request that passes through GameKeyUsageTrackingMiddleware is recorded post-response to bus_game_key_usage. The middleware identifies the game key by reading GameKeyTenantId and GameKeyId from HttpContext.Items. If either is absent, the request is not recorded.
Unlike tenant API key tracking, game key tracking has no SkipUsageTracking opt-out — every authenticated game key request is always recorded.
Fields recorded per request:
| Field | Description |
|---|---|
endpoint | Route template |
path | Actual request path |
method | HTTP method |
request_kind | read (GET/HEAD/OPTIONS) or write (all others) |
status_code | HTTP response status code |
response_time_ms | Elapsed milliseconds from request start to response completion |
request_bytes | Content-Length of the request body (if present) |
response_bytes | Content-Length of the response body (if present) |
ip_address | Client IP address |
user_agent | User-Agent header value |
is_throttled | true if the request was rate-limited (429) |
quota_bucket | Which quota bucket the request was charged against |
billing_outcome | How the request was classified for billing |
billable_units | Number of units charged (0 for non-accepted outcomes) |
billed_workspace_id | Company workspace the usage is attributed to |
occurred_at | UTC timestamp of the request |
Hourly rollup: Game key usage is rolled up into bus_game_key_usage_hourly by a background worker. Each hourly row aggregates: hour_start, request_count, throttled_count, request_bytes. The hourly table backs the hourly analytics query. Daily queries read from the raw table and bucket by day in the service layer.
Billing classification: Game key requests use the same UsageBillingClassifier as tenant API key requests. Bucket is resolved by request_kind: writes use tenant_write_requests, reads use tenant_read_requests. Billing outcomes are identical: only accepted requests consume billable units.
Analytics queries: All endpoints require a SaaS JWT with owner or admin role on the tenant. Daily, hourly, and series endpoints accept an optional keyId parameter to filter to a single game auth key. The ingest-bytes endpoint is tenant-wide and does not accept keyId.
| Endpoint | Granularity | Default range | Key filter |
|---|---|---|---|
/analytics/daily | Per day | Last 90 days | Optional keyId |
/analytics/hourly | Per hour | Last 24 hours | Optional keyId |
/analytics/series | Per day or hour (bucket=day|hour) | Last 90 days (bucket=day by default) | Optional keyId |
/analytics/ingest-bytes | Aggregate over range | Last 90 days | Not supported |
Daily usage response — one bucket per day:
| Field | Description |
|---|---|
date | UTC day |
readCount | GET/HEAD/OPTIONS request count |
writeCount | POST/PUT/PATCH/DELETE request count |
readBytes | Total inbound bytes for reads |
writeBytes | Total inbound bytes for writes |
Hourly usage response — one bucket per hour (contiguous range, zeros included for inactive hours):
| Field | Description |
|---|---|
hourStart | UTC start of the hour |
requestCount | Total requests in that hour |
throttledCount | Number of rate-limited requests |
requestBytes | Total inbound bytes |
Series usage response — one bucket per day or hour:
| Field | Description |
|---|---|
bucketStart | UTC start of the bucket |
readCount | GET/HEAD/OPTIONS request count |
writeCount | POST/PUT/PATCH/DELETE request count |
throttledCount | Number of rate-limited requests |
readBytes | Total inbound bytes for reads |
writeBytes | Total inbound bytes for writes |
estimatedWriteBytes | Write bytes with missing sizes imputed |
totalCount | Total request count |
totalBytes | Total inbound bytes |
estimatedTotalBytes | Total bytes with missing write sizes imputed |
Ingest bytes estimate — tenant-wide only, no keyId filter:
| Field | Description |
|---|---|
estimatedBytes | Total estimated inbound bytes (actual + imputed) |
requestCount | Total number of requests in the range |
imputedRequestCount | Number of requests whose byte size was imputed (no Content-Length) |
Requests with a null request_bytes are imputed using the median of observed byte sizes for that tenant and period.
Date/time parameters (from / to, both optional):
/dailytruncates to date boundaries (time component is ignored)./hourly,/series, and/ingest-bytesuse full timestamps./seriesdefaults tobucket=dayand a 90-day range. Hour series supports at most 24 buckets, so callers usingbucket=hourmust provide a range within that limit.tomust be on or afterfrom— returns 400 otherwise.
Relationship to quota and rate limits: Game key hourly analytics read from rolled-up summary tables. Daily, series, and ingest-byte analytics read or aggregate from recorded usage rows. They reflect the same request stream that drives quota evaluation and alert thresholds, but they are not real-time; rollup lag applies to hourly rollups (up to 10 minutes).
Differences from Tenant API Key Analytics:
| Game Key Analytics | Tenant API Key Analytics | |
|---|---|---|
| Auth header | X-Game-Key | X-API-Key |
| Raw table | bus_game_key_usage | bus_api_key_usage |
| Hourly rollup table | bus_game_key_usage_hourly | — |
| Hourly query source | Rollup table | Raw table (windowed) |
| Per-key detail view | No | Yes — endpoint breakdown, IP list, status distribution |
| Tenant-wide aggregate | No | Yes |
| Ingest bytes estimate | Yes — with imputation | No |
| Tracking opt-out | Not available | SkipApiKeyUsageTracking flag |
Limitations:
- No per-key detail view. Game key analytics only expose time-series (daily/hourly/series) and the ingest bytes estimate.
- Hourly rollup lag. Very recent requests may not appear in hourly results until the next rollup cycle completes.
- Ingest bytes imputation is an estimate.
imputedRequestCountin the response shows how many requests were estimated. - Post-response only. Usage records are best-effort.
Code references:
- Middleware:
GamersLabRestAPI/Saas/ApiKeys/Middleware/GameKeyUsageTrackingMiddleware.cs - Usage model:
GamersLabRestAPI/PlayerAuth/Models/GameKeyUsage.cs - Hourly rollup model:
GamersLabRestAPI/PlayerAuth/Models/GameKeyUsageHourly.cs - Usage service:
GamersLabRestAPI/PlayerAuth/Services/GameKeyUsageService.cs - Rollup worker:
GamersLabRestAPI/PlayerAuth/Services/GameKeyUsageRollupWorkerService.cs - Analytics controller:
GamersLabRestAPI/PlayerAuth/Controllers/GameKeyAnalyticsController.cs - Usage repository:
GamersLabRestAPI/PlayerAuth/Repositories/NpgsqlGameKeyUsageRepository.cs