Gamers Lab Docs

2.2 Sessions & Device Registration

Section 2 — Games

2.2 Sessions & Device Registration

Both the session ledger and device tracking live entirely inside Game/Sessions/.

Player login appends a row to the session ledger and creates/updates mutable session state for runtime freshness checks. Devices are registered at login time as a side effect of the auth flow.

Session Event Types

EventTrigger
LoginSuccessful authentication via any provider
LogoutPlayer explicitly logs out
SessionExpiredService-level timeout event type; not currently written by match validation
ForceLogoutAdmin action; also used for security-triggered evictions
TokenRefreshDefined in code, but the current refresh endpoint does not record this event

GamePlatform Enum

The platform field on sessions and stored device rows uses the GamePlatform enum. Unknown exists as a fallback/default value.

Nintendo

ValueDisplay Name
NintendoSwitchNintendo Switch
NintendoSwitchLiteNintendo Switch Lite
NintendoSwitchOLEDNintendo Switch OLED

PlayStation

ValueDisplay Name
PlayStation4PlayStation 4
PlayStation4ProPlayStation 4 Pro
PlayStation5PlayStation 5
PlayStation5ProPlayStation 5 Pro
PlayStationVRPlayStation VR
PlayStationVR2PlayStation VR2

Xbox

ValueDisplay Name
XboxOneXbox One
XboxOneSXbox One S
XboxOneXXbox One X
XboxSeriesSXbox Series S
XboxSeriesXXbox Series X

PC

ValueDisplay Name
PC_WindowsPC (Windows)
PC_MacPC (Mac)
PC_LinuxPC (Linux)
PC_SteamDeckSteam Deck

Mobile

ValueDisplay Name
Mobile_iOSiOS
Mobile_AndroidAndroid

VR

ValueDisplay Name
MetaQuest2Meta Quest 2
MetaQuest3Meta Quest 3
MetaQuestProMeta Quest Pro
ValveIndexValve Index
HTCViveHTC Vive

Cloud / Other

ValueDisplay Name
Cloud_GeForceNowGeForce NOW
Cloud_XboxCloudXbox Cloud Gaming
Cloud_LunaAmazon Luna
OtherOther
UnknownUnknown

2.2.1 Device Registration

What Device Registration Is

When a player logs into a game, the game client can optionally submit a DeviceInfoRequest alongside its authentication payload. The server uses this to maintain a record of which physical devices have accessed the game under each player account.

Registration is upsert-based: if a device with the same fingerprint already exists for the player, it is updated. If not, a new record is created. The player never calls a registration endpoint directly — it happens as a side effect of login.

When It Runs

Triggered during every login flow where deviceInfo is included. Runs after authentication succeeds and before the player JWT is issued.

Login flows that support device registration:

  • POST /api/player-auth/login (Steam, Epic, Sequence, Mock)
  • POST /api/player-auth/email/signup
  • POST /api/player-auth/email/tenant-login
  • POST /api/player-auth/otc/exchange
  • POST /api/player-auth/wallet/verify (EVM wallet)

Providing deviceInfo is optional. If omitted, login proceeds normally and the session row is created with device_id = null.

Blocked Device Check

Before registration completes, the service checks whether the fingerprint matches a device with is_blocked = true. If so, the login is rejected — the player cannot authenticate from a blocked device regardless of auth provider.

ClientInfoRequest

FieldRequiredMax LengthNotes
platformYesGamePlatform enum value
clientVersionNo32 charsGame client version
clientBuildNo64 charsBuild number or hash
metadataNoArbitrary key-value pairs

DeviceInfoRequest

FieldRequiredMax LengthNotes
deviceFingerprintYes16–256 charsHash of hardware identifiers; unique per physical device+install
hardwareModelNo128 charsDevice model name (e.g. "PlayStation 5 Digital Edition")
osVersionNo64 charsOS version string
metadataNoArbitrary key-value pairs

DeviceName is not part of registration. Players assign a display name later via PATCH /api/player/devices/{deviceId}.

Upsert Behaviour

ConditionResult
Fingerprint not seen beforeNew game_player_devices row created
Fingerprint matches existing devicelast_seen_at updated, login_count incremented, hardware fields updated if changed
Fingerprint matches a blocked deviceLogin rejected — 403

first_seen_at is set only on creation and never updated.

What Gets Stored

Table: game_player_devices

ColumnSource
player_idAuthenticated player
device_fingerprintDeviceInfoRequest.DeviceFingerprint
platformClientInfoRequest.Platform, or Unknown if no client info is provided
hardware_modelDeviceInfoRequest.HardwareModel
os_versionDeviceInfoRequest.OsVersion
is_trustedfalse on creation; player sets via management endpoints
is_blockedfalse on creation; player sets via management endpoints
first_seen_atSet at creation
last_seen_atUpdated on every login
login_countIncremented on every login
metadataDeviceInfoRequest.Metadata

Once registered, the device id is linked to the session row in game_app_player_login_sessions.device_id.

Device Data Model — Full Schema

Table: game_player_devices (mutable)

ColumnTypeNotes
iduuidPK
player_iduuidFK → game_player_profiles.id
device_fingerprinttext16–256 chars; uniqueness enforced per player
platformtextGamePlatform enum value
hardware_modeltext?Device model
os_versiontext?OS version string
device_nametext?Player-assigned display name
is_trustedboolDefault false
is_blockedboolDefault false; blocks device from logging in
first_seen_attimestamptzSet on first registration
last_seen_attimestamptzUpdated on every login from this device
login_countintIncremented on every login from this device
metadatajsonbArbitrary metadata
created_at_gltimestamptz
updated_at_gltimestamptz
received_at_dbtimestamptz

Code References

  • Service interface: GamersLabRestAPI/Game/Sessions/Services/IPlayerDeviceService.cs
    • GetOrCreateDeviceAsync — registration entry point
    • IsDeviceBlockedAsync — blocked check at login
    • RecordDeviceLoginAsync — updates last seen / login count
  • Service implementation: GamersLabRestAPI/Game/Sessions/Services/PlayerDeviceService.cs
  • Repository: GamersLabRestAPI/Game/Sessions/Repositories/PlayerDeviceRepository.cs
  • Device model: GamersLabRestAPI/Game/Sessions/Models/PlayerDevice.cs
  • Device management DTOs: GamersLabRestAPI/Game/Sessions/DTOs/PlayerDeviceDTOs.cs
  • Login telemetry DTOs: GamersLabRestAPI.Contracts/Game/Sessions/DTOs/DeviceInfoRequest.cs
  • Platform enum: GamersLabRestAPI.Contracts/Game/Sessions/Models/GamePlatform.cs

2.2.2 Session Ledger

What the Session Ledger Is

Current implemented session ledger rows are login and logout-style events. Token refresh has a service method and enum value, but POST /api/player-auth/refresh does not currently call it. Nothing in the ledger is ever updated or deleted. It is the authoritative historical record of persisted player auth activity.

The ledger is separate from the mutable session state (game_player_login_session_state), which tracks only current status. The ledger is for history and audit; the state table is for runtime checks.

Two Ledger Tables

Session events are split across two tables by direction:

TableRecordsMutable?
game_app_player_login_sessionsLogin eventsNo — append-only
game_app_player_logout_sessionsLogout, expiry, force-logout eventsNo — append-only

Both carry the same session_id — the link connecting a login row to its logout row. A single logical session can produce a login row and, if explicitly closed, a logout row.

Session ID

session_id is the logical identifier for one player auth session, shared across all ledger rows that belong to it:

Login event        → game_app_player_login_sessions  (session_id = X)
Logout event       → game_app_player_logout_sessions (session_id = X)

session_id is also the PK of the session state row in game_player_login_session_state. All three tables are joined by it.

Event Types and Which Table They Write To

Event typeTableTrigger
Logingame_app_player_login_sessionsSuccessful authentication via any provider
Logoutgame_app_player_logout_sessionsPOST /api/player-auth/logout
SessionExpiredgame_app_player_logout_sessionsService method exists; not currently called by match validation
ForceLogoutgame_app_player_logout_sessionsService method exists for admin/security eviction

Login Ledger — game_app_player_login_sessions

Append-only. Composite PK: (tenant_id, id).

ColumnTypeNotes
tenant_iduuidComposite PK
iduuidComposite PK — unique per row
session_iduuidLinks this row to the session state and logout rows
player_iduuidFK → game_player_profiles.id
auth_method_iduuid?FK → the auth method used; null if not tracked
auth_providertextDenormalised provider name (steam, epic, email, evm_wallet, etc.)
device_iduuid?FK → game_player_devices.id; null if no device submitted
platformtextGamePlatform enum value
client_versiontext?Game client version string
client_buildtext?Build identifier
ip_addressinet?Caller IP at the time of the event
occurred_attimestamptzDomain time — when the event happened
created_at_gltimestamptzServer time — when the platform received it
received_at_dbtimestamptzDatabase time — DEFAULT now()
metadatajsonbArbitrary key-value pairs; immutable
row_hashtextApplication-computed integrity hash

auth_provider is stored denormalised so that queries by provider name do not require a join.

Logout Ledger — game_app_player_logout_sessions

Append-only. Composite PK: (tenant_id, id).

ColumnTypeNotes
tenant_iduuidComposite PK
iduuidComposite PK
session_iduuidLinks to the originating login event
player_iduuidFK → game_player_profiles.id
reasontextlogout_reason enum value
messagetext?Optional free-text context
occurred_attimestamptzDomain time
created_at_gltimestamptzServer receive time
received_at_dbtimestamptzDatabase receive time (DEFAULT now())
metadatajsonbArbitrary metadata captured at logout time

Logout Reasons

ValueMeaning
user_logoutPlayer called POST /api/player-auth/logout
timeoutSession expired (lazily detected)
token_expiredJWT expired without refresh
kickedAdmin force-logout
device_blockedLogin blocked by device block flag
account_suspendedPlayer account blocked platform-wide
network_errorClient-reported network failure
client_crashClient-reported crash
app_closeClient-reported graceful close
server_shutdownServer-initiated termination
unknownFallback

Three Timestamps

Both ledger tables carry three timestamps following the platform's ledger conventions:

TimestampMeaningSet by
occurred_atWhen the event happened in the game worldApplication (domain time)
created_at_glWhen the platform server received and processed itApplication
received_at_dbWhen the database row was insertedDatabase (DEFAULT now())

All three are immutable once written. received_at_db is used as the cursor in paginated history queries.

Row Hash

row_hash is an application-computed hash of the row's content, written at insert time. Provides a deduplication fingerprint and an integrity check.

Query Surfaces

Player Session History — cursor-paginated log of login events for a specific player.

SessionHistoryQuery:

ParameterDefaultNotes
tenantIdOptional — omit for cross-tenant history
cursorTimestampreceived_at_db of last item on previous page
cursorIdid of last item on previous page
pageSize50Items per page

SessionHistoryResponse:

sessions[]
  id                  Guid
  sessionId           Guid
  tenantId            Guid
  authProvider        string
  platform            string
  platformDisplayName string
  deviceId            Guid?
  clientVersion       string?
  clientBuild         string?
  ipAddress           string?
  occurredAt          DateTime
totalCount            int
pageSize              int
hasMore               bool
nextCursorTimestamp   DateTime?
nextCursorId          Guid?

Tenant Session Analytics — aggregate view across all sessions for a tenant.

SessionAnalyticsResponse:

totalLogins           int
totalLogouts          int
uniquePlatforms       int
uniquePlayers         int
loginsByPlatform      Dictionary<string, int>
loginsByProvider      Dictionary<string, int>

Player Session Summary — high-level lifetime summary for a single player across all tenants.

PlayerSessionSummaryResponse:

totalLogins           int
totalGamesPlayed      int         — distinct tenant count
platformsUsed         string[]
firstLoginAt          DateTime?
lastLoginAt           DateTime?
recentSessions[]
  tenantId            Guid
  platform            string
  platformDisplayName string
  clientVersion       string?
  loginAt             DateTime

Relationship to Session State

LedgerSession State (game_player_login_session_state)
MutableNoYes
PurposePermanent history and auditRuntime checks (is this session fresh/active?)
Rows per sessionMany (one per event)One (upserted on each event)
Used forHistory queries, analyticsMatch validation and freshness TTL check
DeletedNeverNo — but ended_at is set on close

A login writes a new ledger row and creates the state row. A logout writes a logout ledger row and sets ended_at on the state row. Token refresh currently rotates tokens only; it does not write a ledger row or update last_seen_at.

Code References

  • Login session model: GamersLabRestAPI/Game/Sessions/Models/PlayerLoginSession.cs
  • Logout session model: GamersLabRestAPI/Game/Sessions/Models/PlayerLogoutSession.cs
  • Session state model: GamersLabRestAPI/Game/Sessions/Models/PlayerLoginSessionState.cs
  • Session event types: GamersLabRestAPI/Game/Sessions/Models/SessionEventType.cs
  • Session service: GamersLabRestAPI/Game/Sessions/Services/PlayerSessionService.cs
    • LogLoginAsync, LogLogoutAsync, LogTokenRefreshAsync, LogSessionExpiredAsync, LogForceLogoutAsync
    • Current refresh endpoint does not call LogTokenRefreshAsync
    • GetPlayerSessionHistoryAsync, GetTenantSessionHistoryAsync, GetTenantAnalyticsAsync, GetPlayerSessionSummaryAsync
  • Session DTOs: GamersLabRestAPI/Game/Sessions/DTOs/PlayerSessionDTOs.cs
  • Session repository: GamersLabRestAPI/Game/Sessions/Repositories/NpgsqlPlayerSessionRepository.cs

2.2.3 Session Heartbeat

What a Player Session Is

When a player authenticates, two things are created:

  1. An append-only ledger row (game_app_player_login_sessions) recording the login event permanently.
  2. A mutable session state row (game_player_login_session_state) tracking whether the session is currently active and when it was last seen.

Logout and force-expiry service methods operate on the state row. The ledger row is never modified. The current refresh endpoint does not update the session state row.

Session Lifetime

A player session has an inactivity window of 2 hours.

The clock is measured from last_seen_at, not from started_at. A session active for 8 hours is still valid as long as last_seen_at is within the last 2 hours.

Session is considered expired when either:

  • ended_at is set (explicit logout or server-terminated), or
  • last_seen_at < now − 2 hours (inactivity timeout)

Note: is_active on the session state model is defined as ended_at == null. A session can satisfy is_active = true and still be expired by the 2-hour TTL check.

Heartbeat — How Sessions Stay Alive

There is no dedicated heartbeat endpoint. Although LogTokenRefreshAsync can update last_seen_at, the current refresh endpoint does not call it.

ActionUpdates last_seen_at
POST /api/player-auth/refreshNo — rotates tokens only in the current implementation
POST /api/player-auth/logoutYes — set to logout time, then ended_at is set
Match create / join (session check)No — validated but not extended
Event writesNo

If the cached session state is older than 2 hours, the player must re-authenticate before session-sensitive writes can succeed.

Expiry Detection — Lazy, Not Scheduled

There is no background service that auto-expires sessions. Expiry is detected lazily the next time a session-sensitive write is attempted.

When expiry is detected:

  • Create-match returns per-player 410 Gone validation errors.
  • Join currently maps the same expired-session validation to 409 Conflict.
  • The current match validation path does not append a SessionExpired logout row or set ended_at.

Until that lazy check triggers, an expired session row sits with ended_at = null and a stale last_seen_at.

What Checks Session Validity

Not all write operations care whether a player session is active.

Player JWT operations — verify the JWT signature and expiry. The JWT has a 2-hour TTL.

Game write operations (X-Game-Key + bearer token) — session validity only checked where semantically meaningful:

OperationSession validity checked?Notes
Match createYesEach player's last_seen_at must be within 2 hours
Match joinYesSame check — currently returned as 409 if session expired
Match leaveNoSelf-only write; no per-player session re-validation
Match endNoBearer auth required; match can end after players' sessions expire
Match resultsNoSelf-only write; written after the match concludes
Event records (match-scoped)Nologin_session_id stored but nullable; not re-validated
Player event recordsNoSame

A player must have an active session to be accepted into a match. Once in, the match can progress and conclude — and all associated events and results can be written — even if that player's session later expires mid-game. A network disruption or client crash should not invalidate in-progress match data.

Session State Data Model

Table: game_player_login_session_state (mutable)

ColumnTypeNotes
tenant_iduuidComposite PK
session_iduuidComposite PK
player_iduuidFK → game_player_profiles.id
started_attimestamptzSet at login; never updated
ended_attimestamptz?Set on logout or by force-expiry service methods; current match validation does not set it
last_seen_attimestamptzSet at login; drives the 2-hour TTL check
auth_providertextProvider used at login
device_iduuid?Device registered at login, if provided
platformtextGamePlatform enum value
client_versiontext?Game client version at login
last_ip_addressinet?Set/updated by session-state writes; not updated by the current refresh endpoint
updated_at_dbtimestamptzDB row update timestamp

Known Limitations

Active session count / concurrent session policy (TODO): There is currently no enforcement of a maximum concurrent session count per player. A player can log in from multiple devices and maintain multiple active sessions simultaneously. A future per-tenant policy could enforce single-session mode or a maximum count.

Proactive expiry (TODO): Expired sessions are not closed automatically. No background service scans for sessions where last_seen_at < now - 2 hours and sets ended_at. Current match validation also does not close the session on expiry. This means:

  • Session analytics may count sessions as "active" past their actual expiry time
  • ended_at is null for sessions that expired without explicit logout

Code References

  • Session state model: GamersLabRestAPI/Game/Sessions/Models/PlayerLoginSessionState.cs
  • Session service: GamersLabRestAPI/Game/Sessions/Services/PlayerSessionService.cs
    • LogTokenRefreshAsync — can update last_seen_at, but is not called by the refresh endpoint
    • LogSessionExpiredAsync — can write a timeout logout event, but is not called by match validation
    • LogLogoutAsync — writes user_logout event
  • Session repository: GamersLabRestAPI/Game/Sessions/Repositories/NpgsqlPlayerSessionRepository.cs
    • UpdateLastSeenAsync — session-state freshness update helper
    • EndSessionAsync — sets ended_at
  • Match session validation: GamersLabRestAPI/Game/Matches/Services/MatchService.cs
    • LoginSessionTtl constant — TimeSpan.FromHours(2)
    • ValidateMatchPlayer — applies session cutoff check; returns 410 if expired
  • Player login/refresh controller: GamersLabRestAPI/PlayerAuth/Controllers/PlayerLoginController.cs

On this page