Players
Overview
Player accounts and ship ownership.
Authentication
- Username/password authentication
- Passwords stored using secure hashing (argon2)
- JWT tokens issued on successful login
- Token used for REST API and WebSocket authentication
Authentication Flow
- Player submits username/password to
/api/auth/login - Server validates credentials
- Server checks if player’s ship exists in physics service
- If ship missing, server calls physics.SpawnShip() to re-create it
- Server returns JWT token
- Player includes token in
Authorization: Bearer <token>header for REST calls - Player passes token as query parameter when opening WebSocket connection
- Token expires after 24 hours
Ship re-spawn on login: If a player’s ship data is missing from Redis (e.g., due to data loss, manual cleanup, or server reset), the players service automatically re-spawns the ship during authentication. This ensures players can always log in and play without manual intervention.
Active ship assignment (#771): When a player has ships but no active ship set in Redis (e.g., after data loss), the first ship is assigned as active using SET NX (set-if-not-exists). This prevents a race condition where concurrent login requests could overwrite each other’s assignment. If SET NX returns false (another request won the race), the existing value is read back and used instead.
Token Refresh
- While connected via WebSocket, api-gateway auto-refreshes token before expiry
- api-gateway calls players.RefreshToken() via gRPC to generate new JWT
- api-gateway sends
token_refreshmessage to client 1 hour before expiry - Client stores new token for future reconnections
- If disconnected for >24 hours, player must re-authenticate via login
Token Refresh Failure Handling
| Scenario | Behavior |
|---|---|
| Refresh succeeds | Client stores new token, continues normally |
| Refresh fails (server error) | Server retries 3 times at 5-minute intervals |
| All retries fail | Connection continues until token expires |
| Token expires while connected | Server sends E005, disconnects client |
| Client reconnects after disconnect | Must re-authenticate via login |
The 1-hour refresh window provides 60 minutes for up to 3 retry attempts (at 0, 5, and 10 minutes) before token expiration.
JWT Signing Key Rotation
Routine rotation:
- Update Kubernetes secret
galaxy-secretswith new JWT signing key - Perform rolling restart of api-gateway and players services
- Old tokens continue working until expiry (max 24 hours)
- New tokens issued with new key
Emergency rotation (key compromise):
- Update Kubernetes secret with new key
- Restart all services immediately (not rolling)
- All existing tokens invalidated instantly
- All connected players disconnected with E005
- Players must re-authenticate via login
No dual-key support in MVP — rotation invalidates old key immediately on restart.
Registration Flow
When a new player registers, the following sequence occurs:
| Step | Service | Action |
|---|---|---|
| 1 | api-gateway | Receives POST /api/auth/register (with optional system_id) |
| 2 | api-gateway | Calls players.Register(username, password, system_id) via gRPC |
| 3 | players | Validates username format (3-20 chars, alphanumeric + underscore) |
| 4 | players | Validates password length (minimum 8 characters) |
| 5 | players | Checks username not already taken |
| 6 | players | Generates player_id and ship_id (UUIDs) |
| 7 | players | Hashes password with argon2 |
| 8 | players | Inserts record into PostgreSQL (within transaction) |
| 9 | players | Calls physics.SpawnShip(ship_id, player_id, username, system_id) |
| 10 | physics | Creates ship in system’s default spawn location |
| 11 | physics | Increments game:total_spawns atomically |
| 12 | physics | Publishes ship.spawned event to Redis Stream |
| 13 | physics | Returns success with initial ShipState |
| 14 | players | Commits PostgreSQL transaction |
| 15 | players | Generates JWT token |
| 16 | players | Returns RegisterResponse with player_id, ship_id, token |
| 17 | api-gateway | Returns token to client |
Failure handling:
| Failure Point | Behavior |
|---|---|
| Validation fails (steps 3-5) | Return error immediately (E010, E011, E012) |
| PostgreSQL insert fails | Return E008 server error |
| physics.SpawnShip fails (any exception) | Rollback PostgreSQL transaction, return E008 |
| After physics succeeds | Transaction commits; registration complete |
Registration is atomic: either both account and ship exist, or neither does. The spawn failure rollback catches any exception (not just gRPC errors) to handle network timeouts, serialization failures, and other transient issues (#772).
JWT Token Preconditions
Before generating a JWT token (at registration, login, or refresh), the players
service must verify that ship_id is a valid UUID and not null. If ship_id
is missing or null, the service must return an error (E008) rather than encoding
a literal "None" string into the token. This applies to all token-issuing code
paths: register, authenticate, and refresh_token.
Player Properties
| Property | Type | Description |
|---|---|---|
| id | uuid | Unique player identifier |
| username | string | Unique display name |
| password_hash | string | Hashed password |
| created_at | timestamp | Account creation time |
Password Reset
- No self-service password reset (MVP)
- Players who forget passwords must contact an administrator
- Admins can reset player passwords via admin dashboard
- New password provided to player out-of-band (e.g., direct message)
Future releases may add email-based self-service reset.
Account Deletion
- Player can delete their account via web client
- Ship is removed immediately upon account deletion
- Username becomes available for reuse
- Action is irreversible
Ship Ownership
Initial Release
- One ship per player (each player controls exactly one ship)
- Up to 16 concurrent connections (MVP infrastructure limit)
- Ship assigned automatically at account creation
- Starting location: Near the system’s default jump gate or station (Sol: Earth orbit, Alpha Centauri: Chiron orbit)
- All new players start in the Sol system (server default)
Connection Limits
| Limit | Type | Value |
|---|---|---|
| Registered accounts | Soft | Unlimited |
| Concurrent connections | Hard | 16 |
- Players can register even when 16 are online (account stored in PostgreSQL)
- When connecting with 16 already online: WebSocket rejected with E013 “Server full”
- Client displays: “Server is full, please try again later”
- Ship state preserved in Redis while disconnected
Future Releases
- Multiple players
- Multiple ships per player
- Ship acquisition mechanics