Gamers Lab Docs

2.3 Matches

Section 2 — Games

2.3 Matches

2.3.1 Match Lifecycle

Overview

A match progresses through up to five discrete steps, each a separate endpoint. Steps are not strictly sequential — results can be submitted before or without an end record — but the typical flow is:

1. Create  →  2. Join (optional, repeated)  →  3. Leave (optional, repeated)
                                            →  4. End  →  5. Results

Match create, join, leave, end, and results are authenticated via both X-Game-Key and a player bearer JWT. POST /api/game/matches/players/resolve is the exception: it is game-key-only and does not require a player JWT.

Ledger Tables

Every match write operation appends to one or more append-only ledger tables. Nothing is ever updated or deleted.

TableWritten byRows
game_app_match_sessionsCreate1 per match
game_app_match_playersCreate (initial players), Join1 per player participation
game_app_match_player_eventsCreate, Join, Leave1+ per player action
game_app_match_endsEnd1 per match
game_app_player_resultsResults1 per player result

Step 1 — Create Match

POST /api/game/matches
X-Game-Key: <key>
Authorization: Bearer <player_jwt>

Creates the match session. Optionally accepts an initial player list.

Request:

FieldRequiredNotes
idempotencyKeyYes1–64 chars
levelNameNoMap, level, or scene name; max 64 chars
isMultiplayerNoBoolean; defaults to false
startedAtYesDomain time the match started (game server clock)
metadataNoArbitrary key-value pairs; max 64 keys, max 32 KB total
players[]NoInitial player list; max 1,000 players

Per player (MatchPlayerRequest):

FieldRequiredNotes
playerIdYesPlayer profile ID
loginSessionIdYesLogin session ID; must be active within 2-hour window
joinedAtNoDefaults to startedAt if omitted
teamIdNoShort integer team identifier
teamLabelNoDisplay label; max 64 chars
isHostNoBoolean; marks the player as match host

Session Validation: Each player's login session must exist and have last_seen_at within the past 2 hours with no ended_at. Failed players are listed in errors[] with status 410 Gone; the match is still created if at least one player is accepted.

Response — Partial Success: Returns 201 Created if the match session was written. 200 OK on idempotency replay.

alreadyProcessed    bool
matchId             Guid?
players[]
  playerId          Guid
  loginSessionId    Guid
  matchPlayerId     Guid?
errors[]
  playerId          Guid
  loginSessionId    Guid?
  statusCode        int
  error             string

What Gets Written:

  • 1 row → game_app_match_sessions
  • 1 row per accepted player → game_app_match_players
  • 1 row per accepted player → game_app_match_player_events (type: joined)

Step 2 — Join Match

POST /api/game/matches/join
X-Game-Key: <key>
Authorization: Bearer <player_jwt>

Adds a single player to an existing match after creation.

Request:

FieldRequiredNotes
idempotencyKeyYes
matchSessionIdYesMust reference an existing, non-ended match
playerIdYes
loginSessionIdYesSession validated same as create
joinedAtNoDefaults to server receive time
teamIdNo
teamLabelNoMax 64 chars
isHostNo

If the player has previously left this match, the join is recorded as a rejoined event.

Response:

alreadyProcessed    bool
matchSessionId      Guid
playerId            Guid
matchPlayerId       Guid
loginSessionId      Guid
joinedAt            DateTime
eventType           string    — "joined" or "rejoined"

What Gets Written:

  • 1 row → game_app_match_players
  • 1 row → game_app_match_player_events (type: joined or rejoined)

Step 3 — Leave Match

POST /api/game/matches/leave
X-Game-Key: <key>
Authorization: Bearer <player_jwt>

Records a player leaving the match. Does not validate the player's login session.

Request:

FieldRequiredNotes
idempotencyKeyYes
matchSessionIdYes
playerIdYes
occurredAtYesDomain time the leave occurred
reasonNoFree-text reason; max 64 chars

Response:

alreadyProcessed    bool
matchSessionId      Guid
playerId            Guid

What Gets Written: 1 row → game_app_match_player_events (type: left)

Step 4 — End Match

POST /api/game/matches/end
X-Game-Key: <key>
Authorization: Bearer <player_jwt>

Records the match as ended. A match can only be ended once.

Request:

FieldRequiredNotes
idempotencyKeyYes
matchSessionIdYes
endReasonYesSee end reason values below
endedAtYesDomain time; max 30 days after startedAt
metadataNoMax 64 keys, max 32 KB

End Reason Values:

ValueMeaning
completedMatch ran to natural conclusion
abandonedMatch abandoned before completion
timeoutMatch exceeded a time limit
server_errorServer-side failure terminated the match
host_disconnectHost player disconnected

Response:

alreadyProcessed    bool
matchSessionId      Guid
endId               Guid

What Gets Written: 1 row → game_app_match_ends

Step 5 — Submit Results

POST /api/game/matches/results
X-Game-Key: <key>
Authorization: Bearer <player_jwt>

Submits outcome data for the authenticated player only. Does not require an end record to exist first. Requires both X-Game-Key and a bearer token, and enforces request.playerId == authenticated playerId.

Request:

FieldRequiredNotes
idempotencyKeyYes
matchSessionIdYes
playerIdYesMust have a match player record in this match
resultYesSee result values below
finalScoreNoLong integer
matchPositionNoInteger finish position
recordedAtYesDomain time the result was determined

Result Values:

ValueMeaning
winPlayer won
lossPlayer lost
drawMatch ended without a winner
forfeitPlayer forfeited
disconnectPlayer disconnected before conclusion
incompleteMatch did not reach a conclusive result for this player
continuePlayer continues to a next stage or round

Response: 201 Created on first success, 200 OK on idempotency replay.

alreadyProcessed    bool
matchSessionId      Guid?
playerId            Guid
matchPlayerId       Guid
resultId            Guid

What Gets Written: 1 row per accepted result → game_app_player_results

Match Player Events

Every player presence action appends a row to game_app_match_player_events. These are structural events about a player's lifecycle within the match — distinct from game event records (game_app_event_records).

Event typeWritten byTrigger
joinedCreate, JoinPlayer accepted into match for the first time
rejoinedJoinPlayer re-joins after a previous leave
leftLeavePlayer leaves voluntarily or is recorded as leaving
kickedMid-match logoutPlayer's session ends while in an active match
timeoutReserved; not currently written by any endpoint

game_app_match_player_events columns:

ColumnTypeNotes
tenant_iduuidComposite PK
iduuidComposite PK
match_session_iduuidFK → match session
match_player_iduuidFK → match player record
login_session_iduuid?Login session at time of event; null if session unavailable
event_typetextjoined, rejoined, left, kicked, timeout
occurred_attimestamptzDomain event time
created_at_gltimestamptzServer receive time
reasontext?Optional context
metadatajsonbArbitrary metadata
idempotency_keytextRequired; used for deduplication
row_hashtextApplication-computed integrity hash

Mid-Match Logout

When a player calls POST /api/player-auth/logout while they have active match player records, AppendLogoutEventsAsync runs automatically. It:

  1. Queries game_app_match_players for all records linked to the player's loginSessionId where no left or kicked event has been written yet.
  2. Appends a kicked event to game_app_match_player_events for each active match.

This keeps the match ledger consistent. The match itself is not affected; it continues for other players.

Field Limits Reference

FieldLimit
levelName64 chars
teamLabel64 chars
Leave reason64 chars
Match metadata keys64 keys
Match metadata size32 KB
endedAtstartedAtMax 30 days
Players per create request1,000
Results per request1,000
Idempotency key64 chars

Error Codes

CodeMeaning
201Created — write accepted
200OK — idempotency replay
400Invalid request
409Conflict — same idempotency key with different payload
410Gone — player login session expired in create-match per-player validation
422Unprocessable — results submission with zero accepted rows
429Rate limit exceeded

Code References

  • Controller: GamersLabRestAPI/Game/Matches/Controllers/GameMatchesController.cs
  • Service interface: GamersLabRestAPI/Game/Matches/Services/IMatchService.cs
  • Service: GamersLabRestAPI/Game/Matches/Services/MatchService.cs
  • Repository interface: GamersLabRestAPI/Game/Matches/Repositories/IMatchRepository.cs
  • Repository: GamersLabRestAPI/Game/Matches/Repositories/NpgsqlMatchRepository.cs
  • DTOs: GamersLabRestAPI.Contracts/Game/Matches/DTOs/MatchDTOs.cs
  • Constants: GamersLabRestAPI/Game/Matches/Models/MatchConstants.cs

2.3.2 Player Resolution

POST /api/game/matches/players/resolve
X-Game-Key: <key>

Resolves a matchPlayerId from a matchSessionId + playerId pair. Used by game servers that need the platform-assigned matchPlayerId for downstream event writes without having cached it from the join/create response.

Request:

FieldRequired
matchSessionIdYes
playerIdYes

Response:

matchSessionId      Guid
playerId            Guid
matchPlayerId       Guid
loginSessionId      Guid?

On this page