Skip to content

Authentication

The server uses two separate auth mechanisms — one for REST, one for WebSocket — backed by Clerk as the identity provider.

REST Authentication — ClerkAuthGuard

File: apps/server/src/auth/clerk-auth.guard.ts
Library: @clerk/backend (verifyToken)

How it works

  1. Reads Authorization: Bearer <token> from the request header
  2. Calls verifyToken(token, { secretKey: CLERK_SECRET_KEY })
  3. Extracts sub claim → sets request.user = { id: sub }
  4. Returns true — handler runs

Failure responses (all HTTP 401)

ConditionMessage
No Bearer token"Missing or invalid Authorization header"
CLERK_SECRET_KEY not set"Server auth not configured (Clerk)"
Token verified but no sub"Invalid token: missing sub"
Verification throws"Invalid or expired token"

Where it's applied

Applied per-controller via @UseGuards(ClerkAuthGuard) — not globally. Every authenticated controller must include the decorator explicitly.

ControllerEndpoints
UsersControllerGET /users/me, PATCH /users/me
MatchesControllerGET /matches, GET /matches/:id, POST /matches/singleplayer, POST /matches/rematch
FriendsControllerAll /friends endpoints
ChallengesControllerGET /challenges
StatisticsControllerGET /statistics, GET /statistics/time-series
XpControllerGET /xp/me

@CurrentUser() decorator

Extracts the authenticated user from request.user inside a handler:

typescript
@Get('me')
async getMe(@CurrentUser() user: AuthUser) {
  // user.id = Clerk user ID
}

WebSocket Authentication — Identify Protocol

Socket.IO connections are not authenticated with a JWT. The design is:

  1. Client connects with no credentials
  2. Client immediately emits identify { playerId, name, expoPushToken? }
  3. MatchmakingGateway maps socketId → userId in PresenceService
  4. All subsequent socket messages resolve identity via presenceService.getUserId(socket.id)

Trust model

AspectDetail
ConnectionOpen — no token required
IdentityClient provides playerId — server takes it at face value
Game messagesInclude playerId in payload; some (not all) are cross-checked against presence
ReconnectClient re-sends identify; old socket entry is overwritten

No JWT is verified on the socket. This is an intentional trade-off for low-latency real-time gameplay. The Clerk JWT is only used for REST calls on the client side.


Admin Key Guard

File: apps/server/src/game-config/game-config.controller.ts (inline)

A simple key-match guard for admin config endpoints:

  • Reads ADMIN_KEY env var
  • Accepts via x-admin-key header or admin_key query param
  • Throws HTTP 401 on mismatch

Applied to: GET /config/full, PUT /config, POST /config/reload, GET /config/rewards, PUT /config/rewards.
GET /config is public (no guard).


Global Configuration

Rate limiting (via ThrottlerGuard, registered as APP_GUARD):

TierLimit
Short30 requests / 10 s per IP
Medium100 requests / 60 s per IP

No global auth guard. Auth is opt-in per controller.

CORS: origin: true, credentials: true (all origins allowed).

Global pipe: ValidationPipe with transform: true, whitelist: true.


Summary

LayerMechanismJWT used?
REST endpointsClerkAuthGuard per-controllerYes — Clerk JWT
WebSocket connectionNoneNo
WebSocket messagesPresence map (socketId → userId)No
Admin config endpointsADMIN_KEY env varNo