REST API
HTTP/JSON API for client-server communication.
Base URL
- Production:
https://galaxy.example.com/api - Development:
https://localhost:30002/api
Authentication
Authenticated endpoints require the Authorization header:
Authorization: Bearer <jwt_token>
Common Response Format
Success
Returns appropriate HTTP status code with JSON body (if applicable).
Error
All errors return JSON with error code and message:
{
"error": {
"code": "E005",
"message": "Authentication failed"
}
}
Endpoints
POST /api/auth/register
Create a new player account.
Request:
{
"username": "PlayerOne",
"password": "securepassword123"
}
| Field | Type | Validation |
|---|---|---|
| username | string | 3-20 characters, alphanumeric + underscore, unique |
| password | string | Minimum 8 characters |
Response (201 Created):
{
"player_id": "550e8400-e29b-41d4-a716-446655440000",
"username": "PlayerOne",
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E011 | Invalid username format |
| 400 | E012 | Password too short |
| 403 | E020 | Registration is currently closed |
| 409 | E010 | Username already taken |
| 500 | E008 | Server error |
POST /api/auth/login
Authenticate and receive JWT token.
Request:
{
"username": "PlayerOne",
"password": "securepassword123"
}
Response (200 OK):
{
"player_id": "550e8400-e29b-41d4-a716-446655440000",
"username": "PlayerOne",
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"is_admin": false
}
Notes:
is_adminis included in the response. For admin accounts, this istrue.ship_idmay be empty string for ship-less admin accounts.- This endpoint works for both regular players and admins (unified login).
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid username or password |
| 429 | E004 | Rate limit exceeded (5 attempts/min) |
| 500 | E008 | Server error |
Security notes:
- Response is identical for wrong username vs wrong password (no enumeration)
- Rate limited to 5 attempts per minute per IP
DELETE /api/account
Delete the authenticated player’s account.
Headers:
Authorization: Bearer <token>
Response (204 No Content)
No response body.
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired token |
| 500 | E008 | Server error |
Side effects:
- Player’s ship is immediately removed from the game
- Username becomes available for reuse
- Action is irreversible
GET /api/player/ship
Get the authenticated player’s current ship state.
Headers:
Authorization: Bearer <token>
Response (200 OK):
{
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"position": {"x": 1.496e11, "y": 0.0, "z": 6771000.0},
"velocity": {"x": 0.0, "y": 29780.0, "z": 7672.0},
"attitude": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
"angular_velocity": {"x": 0.0, "y": 0.0, "z": 0.0},
"thrust_level": 0.0,
"fuel": 10000.0,
"fuel_capacity": 10000.0,
"mass": 20000.0,
"wheel_saturation": {"x": 0.0, "y": 0.0, "z": 0.0}
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired token |
| 404 | E006 | Ship not found |
| 500 | E008 | Server error |
Notes:
wheel_saturationis normalized (0.0–1.0 per axis), derived fromwheel_momentum / wheel_capacity- State is a snapshot at request time; use WebSocket for real-time updates
GET /api/status
Get server status (unauthenticated).
Response (200 OK):
{
"status": "online",
"server_name": "Galaxy",
"tick": 123456,
"game_time": "2025-01-15T10:30:00Z",
"players_online": 5,
"paused": false,
"time_sync_enabled": true,
"time_drift_seconds": 3.5,
"effective_time_scale": 1.0,
"registration_open": true
}
Data sources:
| Field | Source |
|---|---|
| status | “online” if api-gateway is serving requests |
| server_name | SERVER_NAME env var (default "Galaxy") |
| tick | Redis game:tick |
| game_time | Redis game:time |
| players_online | Redis SCARD connections:online |
| paused | Redis game:paused |
| time_sync_enabled | tick-engine gRPC TickStatus.time_sync_enabled |
| time_drift_seconds | tick-engine gRPC TickStatus.time_drift_seconds |
| effective_time_scale | tick-engine gRPC TickStatus.effective_time_scale |
| registration_open | Redis game:registration_open (default true if absent) |
Admin Endpoints
Admin endpoints require a valid admin JWT token.
POST /api/admin/auth/login
Authenticate as administrator. Routes through the unified players.Authenticate gRPC call, then checks is_admin on the response.
Legacy alias:
/api/admin/loginis also supported for backward compatibility. Note: Admins can also log in via/api/auth/login(the unified player login). This endpoint exists for backward compatibility with the admin dashboard.
Request:
{
"username": "admin",
"password": "adminpassword"
}
Response (200 OK):
{
"success": true,
"admin_id": "770e8400-e29b-41d4-a716-446655440002",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"role": "admin",
"is_admin": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid credentials or not an admin |
| 429 | E004 | Rate limit exceeded |
GET /api/admin/players
List all registered players.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"players": [
{
"player_id": "550e8400-e29b-41d4-a716-446655440000",
"username": "PlayerOne",
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"created_at": "2025-01-10T08:00:00Z",
"online": true
}
]
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
DELETE /api/admin/players/{player_id}
Delete a player account (admin action).
Headers:
Authorization: Bearer <admin_token>
Response (204 No Content)
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 404 | E009 | Player not found |
Side effects:
- If player is connected, they receive E014 and are disconnected
- Ship is removed immediately
POST /api/admin/players/{player_id}/reset-password
Reset a player’s password.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"new_password": "temporarypassword123"
}
Response (200 OK):
{
"message": "Password reset successfully"
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E012 | Password too short |
| 401 | E005 | Invalid or expired admin token |
| 404 | E009 | Player not found |
Admin provides the new password to the player out-of-band.
PUT /api/admin/players/{player_id}/role
Promote a player to admin or demote an admin to player.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"is_admin": true
}
| Field | Type | Description |
|---|---|---|
| is_admin | boolean | true to promote to admin, false to demote to player |
Response (200 OK):
{
"success": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | Cannot change own admin role |
| 400 | - | Player service returned error |
| 401 | E005 | Invalid or expired admin token |
Constraints:
- Admins cannot change their own role (self-demotion is blocked)
- Promoted players keep their existing ship
- Demoted admins with no ship cannot log in normally until a ship is assigned
GET /api/admin/status
Get current game status including tick info, rates, drift, and sync state.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"current_tick": 123456,
"tick_rate": 1.0,
"time_scale": 1.0,
"actual_rate": 0.998,
"paused": false,
"ticks_behind": 0,
"game_time": "2025-01-15T10:30:00+00:00",
"energy_drift": 1.2e-8,
"momentum_drift": 3.4e-9,
"time_sync_enabled": true,
"time_drift_seconds": 0.5,
"effective_time_scale": 1.002,
"adaptive_tick_rate": false,
"registration_open": true
}
| Field | Type | Description |
|---|---|---|
| current_tick | integer | Current tick number |
| tick_rate | number | Configured ticks per second |
| time_scale | number | Configured time scale multiplier |
| actual_rate | number | Actual achieved tick rate |
| paused | boolean | Whether tick processing is paused |
| ticks_behind | integer | Number of ticks behind schedule (0 = healthy) |
| game_time | string | Current game time (ISO 8601) |
| energy_drift | number | Relative energy drift (dE/E0) |
| momentum_drift | number | Relative angular momentum drift (dL/L0) |
| time_sync_enabled | boolean | Whether time sync to UTC is active |
| time_drift_seconds | number | Game time drift from wall-clock UTC (seconds) |
| effective_time_scale | number | Actual time scale after sync adjustment |
| adaptive_tick_rate | boolean | Whether adaptive tick rate is enabled |
| registration_open | boolean | Whether new player registrations are allowed |
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure communicating with tick-engine |
POST /api/admin/game/pause
Pause tick processing.
Response (200 OK):
{
"paused": true,
"paused_at_tick": 123456
}
POST /api/admin/game/resume
Resume tick processing.
Response (200 OK):
{
"paused": false,
"resumed_at_tick": 123456
}
PUT /api/admin/game/tick-rate
Set tick rate.
Request:
{
"tick_rate": 2.0
}
Response (200 OK):
{
"previous_rate": 1.0,
"new_rate": 2.0
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E015 | Invalid tick rate (outside 0.1-100.0 range, or zero) |
PUT /api/admin/game/time-scale
Set time scale (game speed multiplier).
Request:
{
"time_scale": 10.0
}
Response (200 OK):
{
"previous_scale": 1.0,
"new_scale": 10.0
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E029 | Invalid time scale (outside 0.1-100.0 range, or zero) |
POST /api/admin/game/time-sync
Toggle game time synchronization to wall-clock UTC.
Request:
{
"enabled": true
}
Response (200 OK):
{
"success": true,
"previous": false,
"current": true
}
When enabled and admin time_scale ≈ 1.0, the tick engine adjusts effective time scale (±5% max) to converge game time to UTC. See Game Time Synchronization for details.
POST /api/admin/game/adaptive-tick-rate
Toggle adaptive tick rate. When enabled, the tick engine automatically adjusts tick rate based on processing utilization (target 70%, range 0.5–10 Hz).
Request:
{
"enabled": true
}
Response (200 OK):
{
"success": true,
"adaptive_tick_rate": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure communicating with tick-engine |
GET /api/admin/systems
List all star systems with dynamic entity counts.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"systems": [
{
"id": "sol",
"name": "Sol",
"star_body": "Sun",
"position": {"x": 0.0, "y": 0.0, "z": 0.0},
"body_count": 10,
"player_count": 3
}
]
}
| Field | Type | Description |
|---|---|---|
| id | string | System identifier |
| name | string | Display name |
| star_body | string | Primary star body name |
| position | vec3 | Galactic position (meters) |
| body_count | integer | Number of celestial bodies (from Redis system:{id}:bodies set) |
| player_count | integer | Number of ships in system (from Redis system:{id}:ships set) |
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure communicating with galaxy service |
POST /api/admin/ship/{ship_id}/system
Move a ship to a different star system (admin testing tool). Updates the ship’s system_id in Redis and moves it between system ship sets.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| ship_id | string | UUID of the ship to move |
Request:
{
"system_id": "alpha_centauri"
}
Response (200 OK):
{
"success": true,
"previous_system": "sol",
"new_system": "alpha_centauri"
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | system_id is required, unknown system, or ship already in that system |
| 401 | E005 | Invalid or expired admin token |
| 404 | - | Ship not found |
POST /api/admin/registrations
Set whether new player registrations are allowed.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"open": true
}
Response (200 OK):
{
"success": true,
"registration_open": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
GET /api/admin/registrations
Get current registration status.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"registration_open": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
POST /api/admin/registrations/enable
Enable new player registrations. Sets game:registration_open to "true" in Redis.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"success": true,
"registration_open": true
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
POST /api/admin/registrations/disable
Disable new player registrations. Sets game:registration_open to "false" in Redis. Players who attempt to register while registrations are disabled receive error E020.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"success": true,
"registration_open": false
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
POST /api/admin/game/reset
Factory reset the game (destructive). Wipes all ships, player accounts, automation rules, maneuvers, game config, and snapshots. A backup is created before destruction.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"success": true,
"game_time": "2025-01-15T10:30:00+00:00",
"backup_id": "20250115T103000Z",
"restart_required": true
}
| Field | Type | Description |
|---|---|---|
| success | boolean | Whether the reset succeeded |
| game_time | string | New game time after reset (ISO 8601, current UTC) |
| backup_id | string | Identifier for the pre-reset backup |
| restart_required | boolean | Whether services should be restarted for clean state |
Side effects:
- Admin account is automatically re-bootstrapped from env vars (no restart needed for admin access)
- All player WebSocket connections remain open but subsequent commands return E006
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure or reset error |
POST /api/admin/snapshot
Create a manual snapshot.
Response (200 OK):
{
"success": true,
"snapshot_id": 42,
"tick_number": 123456
}
GET /api/admin/snapshots
List available snapshots.
Response (200 OK):
{
"snapshots": [
{
"id": 42,
"tick_number": 123456,
"game_time": "2000-01-01T12:00:00+00:00",
"created_at": "2025-01-15T10:30:00+00:00"
}
]
}
POST /api/admin/restore
Restore game state from a snapshot.
Request:
{
"snapshot_id": 42
}
Response (200 OK):
{
"success": true,
"tick_number": 123456,
"game_time": "2000-01-01T12:00:00+00:00"
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 404 | E016 | Snapshot not found |
POST /api/admin/admins
Create a new admin account.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"username": "newadmin",
"password": "securepassword123"
}
Response (201 Created):
{
"admin_id": "880e8400-e29b-41d4-a716-446655440003",
"username": "newadmin"
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E011 | Invalid username format |
| 400 | E012 | Password too short |
| 409 | E010 | Username already taken |
DELETE /api/admin/admins/{admin_id}
Delete an admin account.
Headers:
Authorization: Bearer <admin_token>
Response (204 No Content)
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | Cannot delete self |
| 404 | E009 | Admin not found |
PUT /api/admin/admins/{admin_id}/password
Change an admin’s password.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"new_password": "newsecurepassword123"
}
Response (200 OK):
{
"message": "Password changed successfully"
}
Notes:
- Admins can change their own password or another admin’s password
- Changing own password does not invalidate current token
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | E012 | Password too short |
| 404 | E009 | Admin not found |
GET /api/admin/admins
List all admin accounts.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"admins": [
{
"admin_id": "770e8400-e29b-41d4-a716-446655440002",
"username": "admin",
"created_at": "2025-01-01T00:00:00Z"
}
]
}
POST /api/admin/station
Spawn a new station. Supports two modes: equatorial orbit (specify altitude) or Lagrange point placement.
Headers:
Authorization: Bearer <admin_token>
Request (equatorial orbit):
{
"name": "Gateway Station",
"parent_body": "Earth",
"altitude": 5500000
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Station display name |
| parent_body | string | yes | Reference body name (e.g., “Earth”) |
| altitude | number | conditional | Altitude above body surface in meters (required for equatorial orbit mode) |
Request (Lagrange point):
{
"name": "Frontier Outpost",
"parent_body": "Earth",
"secondary_body": "Luna",
"lagrange_point": 5
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Station display name |
| parent_body | string | yes | Primary body name |
| secondary_body | string | conditional | Secondary body name (required for Lagrange point mode) |
| lagrange_point | integer | conditional | Lagrange point number 1–5 (required for Lagrange point mode) |
Response (200 OK):
{
"success": true,
"station_id": "990e8400-e29b-41d4-a716-446655440004",
"station": {
"station_id": "990e8400-e29b-41d4-a716-446655440004",
"name": "Gateway Station",
"position": {"x": 1.496e11, "y": 0.0, "z": 1.1871e7},
"velocity": {"x": 0.0, "y": 29780.0, "z": 5790.0},
"attitude": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
"mass": 420000,
"radius": 50,
"parent_body": "Earth"
}
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | Invalid parameters (missing fields, unknown body, invalid Lagrange point) |
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure communicating with physics service |
DELETE /api/admin/station/{station_id}
Remove a station.
Headers:
Authorization: Bearer <admin_token>
Response (204 No Content)
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 404 | - | Station not found |
| 500 | E008 | gRPC failure communicating with physics service |
GET /api/admin/stations
List all stations.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"stations": [
{
"station_id": "990e8400-e29b-41d4-a716-446655440004",
"name": "Gateway Station",
"position": {"x": 1.496e11, "y": 0.0, "z": 1.1871e7},
"velocity": {"x": 0.0, "y": 29780.0, "z": 5790.0},
"attitude": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
"mass": 420000,
"radius": 50,
"parent_body": "Earth"
}
]
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
POST /api/admin/jumpgate
Spawn a new jump gate at a Lagrange point.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"name": "Earth-Luna L4 Gate",
"primary_body": "Earth",
"secondary_body": "Luna",
"lagrange_point": 4
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Jump gate display name (non-empty) |
| primary_body | string | yes | Primary body name (non-empty) |
| secondary_body | string | yes | Secondary body name (non-empty) |
| lagrange_point | integer | no | Lagrange point number 1–5 (default: 4) |
All fields are validated server-side via Pydantic (SpawnJumpGateRequest model). Missing required fields or invalid lagrange_point values return 422.
Response (200 OK):
{
"success": true,
"jumpgate_id": "aa0e8400-e29b-41d4-a716-446655440005",
"jumpgate": {
"jumpgate_id": "aa0e8400-e29b-41d4-a716-446655440005",
"name": "Earth-Luna L4 Gate",
"position": {"x": 1.496e11, "y": 0.0, "z": 0.0},
"velocity": {"x": 0.0, "y": 29780.0, "z": 0.0},
"mass": 100000,
"radius": 25,
"parent_body": "Earth"
}
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | Invalid parameters (missing fields, unknown body, invalid Lagrange point) |
| 401 | E005 | Invalid or expired admin token |
| 500 | E008 | gRPC failure communicating with physics service |
DELETE /api/admin/jumpgate/{jumpgate_id}
Remove a jump gate.
Headers:
Authorization: Bearer <admin_token>
Response (204 No Content)
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
| 404 | - | Jump gate not found |
| 500 | E008 | gRPC failure communicating with physics service |
GET /api/admin/jumpgates
List all jump gates.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"jumpgates": [
{
"jumpgate_id": "aa0e8400-e29b-41d4-a716-446655440005",
"name": "Earth-Luna L4 Gate",
"position": {"x": 1.496e11, "y": 0.0, "z": 0.0},
"velocity": {"x": 0.0, "y": 29780.0, "z": 0.0},
"mass": 100000,
"radius": 25,
"parent_body": "Earth"
}
]
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
Maneuver Logging Endpoints
GET /api/admin/maneuver-logging
Get the current global maneuver logging level.
Headers:
Authorization: Bearer <admin_token>
Response (200 OK):
{
"level": "normal"
}
| Field | Type | Description |
|---|---|---|
| level | string | Current logging level: "verbose", "normal", or "off" |
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
POST /api/admin/maneuver-logging
Set the global maneuver logging level.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"level": "verbose"
}
| Field | Type | Description |
|---|---|---|
| level | string | "verbose", "normal", or "off" |
Response (200 OK):
{
"success": true,
"level": "verbose"
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 400 | - | Invalid level value |
| 401 | E005 | Invalid or expired admin token |
WebSocket Messages
ship_rename
Rename the player’s ship. The new name propagates to all clients via the next tick broadcast.
Client sends:
{
"type": "ship_rename",
"name": "My Ship Name"
}
| Field | Type | Validation |
|---|---|---|
| name | string | 3-32 characters after whitespace trim, non-empty |
Server responds (success):
{
"type": "ship_renamed",
"name": "My Ship Name"
}
Server responds (validation failure):
{
"type": "error",
"code": "E030",
"message": "Ship name must be 3-32 characters"
}
Implementation notes:
- api-gateway writes directly to Redis
ship:{ship_id}hashnamefield - No gRPC call needed — physics reads
namefrom Redis each tick - Name appears in tick broadcasts via
ShipState.nameautomatically
Client-Side Security
Client code must never interpolate server-provided strings (body names, player usernames, ship IDs) directly into innerHTML. Use textContent for plain text, or escape &, <, >, ", and ' as HTML entities before inserting into HTML markup. This applies to all server-provided data regardless of server-side validation, as a defense-in-depth measure.
JWT Token Security
Token Generation
| Token Type | Generated By | Description |
|---|---|---|
| Player | players service | On successful login or registration |
| Admin | api-gateway | On successful admin login |
Signing key:
- Algorithm: HS256 (HMAC-SHA256)
- Key source: Kubernetes Secret (
galaxy-secrets) - Key environment variable:
JWT_SECRET_KEY - Key length: Minimum 256 bits (32 bytes)
- Key rotation: Not supported in initial release (requires redeployment)
- Startup validation: Services must fail fast at startup if
JWT_SECRET_KEYis not set or is shorter than 32 bytes. This prevents silent operation with an empty or weak signing key.
Configuration security:
Sensitive configuration fields (jwt_secret_key, postgres_password, database_url, ssl_keyfile) must be excluded from log output and object repr. Services must use Field(repr=False) on sensitive Pydantic settings fields and must not log connection URLs that embed credentials.
Services that need the signing key:
| Service | Usage |
|---|---|
| players | Signs player JWTs |
| api-gateway | Signs admin JWTs, validates all JWTs |
Token validation:
The api-gateway validates all incoming JWTs:
- Check signature using shared secret
- Check expiry (
expclaim) - Check token type (
typeclaim matches expected) - Extract claims (
sub,ship_idfor players)
Invalid tokens return E005 error.
JWT Token Structure
Player Token
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"type": "player",
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"iat": 1705315800,
"exp": 1705402200
}
| Claim | Description |
|---|---|
| sub | Player UUID |
| type | “player” |
| ship_id | Player’s ship UUID |
| iat | Issued at (Unix timestamp) |
| exp | Expires at (24 hours after issue) |
Admin Token
{
"sub": "770e8400-e29b-41d4-a716-446655440002",
"type": "admin",
"iat": 1705315800,
"exp": 1705402200
}
| Claim | Description |
|---|---|
| sub | Admin UUID |
| type | “admin” |
| iat | Issued at (Unix timestamp) |
| exp | Expires at (24 hours after issue) |
Maneuver Diagnostics Endpoints
GET /api/admin/maneuver-events
Query persistent maneuver lifecycle events from PostgreSQL. Uses AdminAuth.fetch() for database access (not private pool attribute).
Headers:
Authorization: Bearer <admin_token>
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| ship_id | uuid | no | - | Filter by ship |
| event_type | string | no | - | Filter by event type |
| limit | integer | no | 50 | Max rows (1-500) |
Response (200 OK):
{
"events": [
{
"id": 1,
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"maneuver_type": "rendezvous",
"event_type": "maneuver_started",
"phase": "plane_change",
"from_phase": null,
"tick": 123456,
"game_time": "2025-01-15T10:30:00+00:00",
"element_errors": null,
"distance": null,
"effectivity": null,
"status_text": null,
"extra": {"ref_body": "Earth", "target_id": "..."},
"created_at": "2025-01-15T10:30:00+00:00"
}
]
}
Errors:
| Status | Code | Condition |
|---|---|---|
| 401 | E005 | Invalid or expired admin token |
GET /api/admin/maneuver-debug
Get per-ship maneuver debug flag.
Headers:
Authorization: Bearer <admin_token>
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| ship_id | uuid | yes | Ship to query |
Response (200 OK):
{
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"debug": true
}
POST /api/admin/maneuver-debug
Toggle per-ship maneuver diagnostic logging.
Headers:
Authorization: Bearer <admin_token>
Request:
{
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"enabled": true
}
Response (200 OK):
{
"success": true,
"ship_id": "660e8400-e29b-41d4-a716-446655440001",
"debug": true
}