This guide covers the player session tracking system — how poker play sessions are recorded, how table join/leave events are tracked, and how session statistics are calculated.
Not to be confused with auth sessions (login/JWT). Player sessions track time spent playing poker at tables.
Architecture Overview
PlayerSession — one per continuous play period
└── PlayerSessionTable[] — one per table joined during the session
Source files:
| File | Purpose |
|---|---|
src/services/player-session.service.ts | Core service: lifecycle, stats, history, orphan healing |
Data Model
PlayerSession
| Field | Type | Description |
|---|---|---|
id | string | Primary key |
userId | string | Owner |
startedAt | DateTime | When the session began |
endedAt | DateTime? | When the session ended (null = active) |
totalBuyIn | Decimal | Sum of all table buy-ins |
totalCashOut | Decimal | Sum of all table cash-outs |
handsPlayed | int | Hands played during session |
rake | Decimal | Total rake paid |
PlayerSessionTable
| Field | Type | Description |
|---|---|---|
id | string | Primary key |
sessionId | string | FK → PlayerSession (cascade delete) |
tableId | string | Table identifier (no database relation) |
joinedAt | DateTime | When the player sat down |
leftAt | DateTime? | When the player left (null = still seated) |
buyIn | Decimal | Amount bought in for |
cashOut | Decimal? | Amount cashed out |
Session Lifecycle
1. Start Session
import { startSession, getOrCreateActiveSession } from './services/player-session.service.js';
// Explicit start
const session = await startSession(userId);
// Or get-or-create (idempotent)
const session = await getOrCreateActiveSession(userId);
A user has at most one active session at a time.
2. Join / Leave Tables
import { addTableToSession, leaveTable } from './services/player-session.service.js';
// Player sits down
const sessionTable = await addTableToSession({
sessionId: session.id,
tableId: 'table-uuid',
buyIn: 100,
});
// Player stands up
await leaveTable(sessionTable.id, cashOut);
3. Accumulate Stats
import { incrementHandsPlayed, addRake } from './services/player-session.service.js';
await incrementHandsPlayed(sessionId); // +1 hand (optional count argument)
await addRake(sessionId, rakeAmount); // accumulate rake
4. End Session
Sessions end in two ways:
- Auto-end:
checkAndEndSessionIfComplete(userId)ends the active session when all its tables haveleftAtset. The WebSocket layer calls it when a player leaves a table. - Manual:
endSession(sessionId)calculates final stats and closes the session.
EndSessionResult includes totalBuyIn, totalCashOut, netProfit, handsPlayed, rake, durationMinutes, tableCount, hourlyRate (null for sessions under a minute), biggestWin and biggestLoss. Ending a session that has already ended returns its stats without changing it.
Orphan Healing
checkAndHealActiveTables(userId) detects and auto-closes orphaned PlayerSessionTable records — entries that claim leftAt = null but have no corresponding active TableSession (e.g., after a server crash).
This function:
- Batch-fetches active
TableSessionrecords for all claimed-active tables (single query) - Closes orphaned
PlayerSessionTablerecords in a batchupdateMany, settingleftAtto now andcashOutto 0 - Ends the parent
PlayerSessionif all tables were orphaned
It returns true when the player has at least one genuinely active table. The withdrawal and transfer services call it to verify a player is not seated before allowing financial operations.
Naming note: This function was previously named
hasActiveTables. The old name is still exported as a deprecated alias, and the withdrawal and transfer services still import it under that name.
Session History API
List Sessions
getSessionList(userId, filters, pagination) returns paginated session history with optional filters:
| Filter | Type | Description |
|---|---|---|
startDate | Date? | Sessions started after this date |
endDate | Date? | Sessions started before this date |
result | 'win' | 'loss' | 'all' | Filter by net profit sign |
tableMode | string? | Filter by table mode (REAL_MONEY, VIRTUAL_CHIPS, TOURNAMENT) |
Aggregate Stats
getSessionAggregateStats(userId, filters) returns lifetime statistics:
- Total/completed sessions, total profit, total hands played, total rake, total duration
- Win/loss/break-even counts and win rate
- Average hourly rate and session duration
- Biggest single-session win and loss
Both are exposed to players by src/api/routes/user/sessions.routes.ts as GET /poker-sessions and GET /poker-sessions/stats, with single-session details at GET /poker-sessions/:id.
Usage by Other Services
| Consumer | Usage |
|---|---|
src/services/withdrawal.service.ts | Blocks withdrawals while player is seated (hasActiveTables alias) |
src/services/transfer.service.ts | Blocks transfers while player is seated (hasActiveTables alias) |
src/websocket/game-events/player-actions.ts | Adds a table on join (addTableToSession) and ends completed sessions on leave (checkAndEndSessionIfComplete) |
src/websocket/table-manager.ts | Counts hands (incrementHandsPlayed) |