Skip to content

Server

The backend is a NestJS 11 application (apps/server/) using TypeScript. It exposes a REST API and two Socket.IO gateways.

Module Map

ModulePathResponsibility
authsrc/auth/Clerk JWT validation; guards for REST and Socket endpoints
gamesrc/game/Core game logic: move validation, state, timers, reveal, rematch
game-configsrc/game-config/Reading/writing appConfig key-value settings
matchessrc/matches/Match REST endpoints and Clerk name enrichment
matchmakingsrc/matchmaking/Queue management, player matching, challenge flow, rematch
challengessrc/challenges/Challenge invitation persistence
friendssrc/friends/Friend relationships and invitation management
spotifysrc/spotify/Spotify API integration for band name validation
statisticssrc/statistics/Aggregated stats and time-series data
userssrc/users/User profiles
xpsrc/xp/XP totals, leveling, reward event ledger
presencesrc/presence/Online/offline status; socket-to-userId mapping
notificationssrc/notifications/Push notification dispatch
pushsrc/push/Expo push token management
databasesrc/database/Drizzle ORM setup and connection

Key Service Files

FilePurpose
game/game.service.tsMove validation, band chain rules, game state transitions, timer, timeout
game/game.gateway.tsSocket.IO event handlers for all in-match events
game/game-state-cache.service.tsIn-memory game state cache (5 min TTL)
game/constants.tsBOT_ID, BOT_DISPLAY_NAME, timer constants
matchmaking/matchmaking.service.tsIn-memory queue (Map), 2-player matching
matchmaking/matchmaking.gateway.tsSocket.IO events: identify, queue, challenge, rematch
matchmaking/match-start.service.tsMatch document creation and player notification
matchmaking/rematch.service.tsRematch creation (reorders players, same gameMode)
spotify/spotify.service.tsSpotify search, "The" prefix handling, token refresh
xp/xp.service.tsXP award, discovery bonus check, level computation
xp/xp.utils.tsxpForLevel(), levelFromXp(), xpToNextLevel()

Database Schema

Managed with Drizzle ORM. Migrations are in apps/server/neon/.

TableKey ColumnsPurpose
matchesid, players[], status, currentTurn, moves[], maxTurnTimeMs, winnerActive and historical game state
bandsid, matchId, playerId, bandName, isValid, revealed, round, followerCountEach band played; id format {gameId}_{timestamp}_{random9}
friendsuserId, friendIdAccepted friend relationships (unique constraint)
friendInvitationsfromUserId, toUserId, statusPending/accepted/declined friend requests
challengesid, fromUserId, toUserId, matchId, statusChallenge invitations linked to matches
appConfigkey, value (JSON)Global app settings — keys: 'match-config', 'menu-buttons', 'store-items', 'xp-config'
userXpuserId, totalXp, levelXP totals and cached level per user
rewardEventsuserId, eventType, matchIdLedger preventing double-awarding
itemsid, name, description, typeItem catalog
userItemsuserId, itemId, quantityUser inventory
pushTokensuserId, token, platformExpo push notification tokens

Game State Cache

GameStateCacheServicein-memory only, not Redis.

PropertyValue
StorageMap<string, CachedGameState>
TTL5 minutes (300 000 ms)
Cleanup interval1 minute (60 000 ms)
Cache missFalls back to database fetch

On cache miss, Firestore timestamps are converted to JS Date objects and legacy fields are backfilled:

  • move.valid missing → true
  • move.round missing → 1
  • gameState.currentRound missing → 1

Cache is invalidated when player socketId changes or match document is updated.

Matchmaking Queue

MatchmakingServicein-memory only, not Redis.

typescript
queues: Map<string, MatchmakingQueue>       // gameMode → queue
playerQueues: Map<string, string>           // playerId → queueId (for fast lookup)

Queue ID equals the gameMode string ('default', 'lite', 'singleplayer').

Match threshold: 2 players in the same queue triggers createMatchWithPlayers(). The first 2 players are spliced out, leaving any additional waiters in the queue.

Match ID Format

text
match_{Date.now()}_{randomString9chars}

Example: match_1716825600000_a3f9z2k1x

XP System

See XP & Ranks for formulas, bonuses, and rank names.

Reward event types (stored in rewardEvents to prevent double-awarding):

  • Match win → base XP + bonuses per band
  • Match loss → base loss XP

XP config is stored in appConfig under key 'xp-config' and loaded into memory at startup. Changes via PUT /config/rewards take effect immediately.

Push Notifications

Push notifications fire when:

  • A move is completed AND the next player is not the mover AND the next player is not in the foreground

Message format:

text
Title: "Your turn"
Body:  "{moverName} played a band"
Data:  { type: 'match_move', matchId }

Player foreground state is tracked via appStateChange socket event from the client.

Environment Variables

VariableUsed byPurpose
SPOTIFY_CLIENT_IDSpotifyServiceSpotify API credential
SPOTIFY_CLIENT_SECRETSpotifyServiceSpotify API credential
ADMIN_KEYGameConfigControllerAdmin endpoint protection (x-admin-key header)

See Environment Variables for the complete server env var list including optional logging flags and stale keys.

Existing Documentation

  • apps/server/docs/challenge-server.md — Socket events for challenge flow with full payloads
  • apps/server/docs/statistics-api.md — REST endpoints for stats
  • apps/server/docs/logging.md — Logging setup and log levels