Redis Data Structure
Real-time game state storage in Redis.
Key Naming Convention
| Key Pattern | Type | Description |
|---|---|---|
ship:{ship_id} |
Hash | Ship real-time state |
station:{station_id} |
Hash | Station real-time state |
body:{name} |
Hash | Celestial body state |
game:tick |
String | Current tick number |
game:time |
String | Current game time (ISO 8601) |
game:total_spawns |
String | Total spawn counter |
game:paused |
String | Pause state (“true”/”false”) |
game:tick_rate |
String | Current tick rate (ticks per second) |
game:time_scale |
String | Current time scale multiplier (1.0 = real-time) |
game:time_sync |
String | Time sync enabled (“true”/”false”) |
game:time_drift |
String | Current UTC drift in seconds (positive = game behind) |
game:registration_open |
String | Registration open (“true”/”false”, default open if absent) |
maneuver:{ship_id} |
Hash | Active maneuver state for a ship |
automation:{ship_id}:rules |
Set | Rule IDs for a ship |
automation:{ship_id}:{rule_id} |
Hash | Automation rule definition |
connections:online |
Set | Player IDs with active WebSocket connections |
system:{system_id}:bodies |
Set | Body names belonging to a star system |
system:{system_id}:ships |
Set | Ship UUIDs in a star system |
system:{system_id}:stations |
Set | Station UUIDs in a star system |
system:{system_id}:jumpgates |
Set | Jump gate UUIDs in a star system |
Ship State Hash
Key: ship:{ship_id}
| Field | Type | Description |
|---|---|---|
| player_id | string | Owner’s player UUID |
| name | string | Ship display name (editable, default = player username at spawn) |
| ship_class | string | Ship class identifier (“fast_frigate” or “cargo_hauler”), default “fast_frigate” |
| position | JSON array | [x, y, z] meters |
| velocity | JSON array | [x, y, z] m/s |
| attitude | JSON array | [w, x, y, z] quaternion |
| angular_velocity | JSON array | [x, y, z] rad/s |
| rotation_input | JSON array | [pitch, yaw, roll] -1.0 to 1.0, current player input |
| thrust_level | string | 0.0 to 1.0 |
| attitude_hold | string | “true” or “false”, rate damping mode |
| attitude_mode | string | “none”, “hold”, “prograde”, “retrograde”, “normal”, “antinormal”, “radial”, “antiradial”, “target”, “target_retrograde” |
| attitude_target_w/x/y/z | string | Target quaternion components (when attitude_mode requires target) |
| attitude_target_id | string | Target entity ID (body name, ship_id, station_id, or lagrange key) for target modes |
| attitude_target_type | string | “body”, “ship”, “station”, or “lagrange” |
| fuel | string | kg remaining |
| fuel_capacity | string | kg maximum capacity |
| dry_mass | string | kg (mass without fuel) |
| wheel_momentum | JSON array | [x, y, z] N·m·s |
| system_id | string | Star system identifier (default “sol”) |
Example:
HSET ship:550e8400-e29b-41d4-a716-446655440000 \
player_id "660e8400-e29b-41d4-a716-446655440001" \
name "PlayerOne" \
ship_class "fast_frigate" \
position "[1.5e11, 0, 0]" \
velocity "[0, 29780, 0]" \
attitude "[1, 0, 0, 0]" \
angular_velocity "[0, 0, 0]" \
rotation_input "[0, 0, 0]" \
thrust_level "0.0" \
attitude_hold "false" \
fuel "30000" \
wheel_momentum "[0, 0, 0]"
Celestial Body Hash
Key: body:{name} (e.g., body:Earth)
| Field | Type | Description |
|---|---|---|
| position | JSON array | [x, y, z] meters |
| velocity | JSON array | [x, y, z] m/s |
| system_id | string | Star system identifier (default “sol”) |
Static properties (mass, radius, type, color) are loaded from configuration, not stored in Redis.
Example:
HSET body:Earth \
position "[-2.627e10, 1.445e11, -1.038e4]" \
velocity "[-2.983e4, -5.220e3, 0]"
Station State Hash
Key: station:{station_id}
| Field | Type | Description |
|---|---|---|
| station_id | string | Station UUID |
| name | string | Human-readable name |
| position_x | string | X position in meters |
| position_y | string | Y position in meters |
| position_z | string | Z position in meters |
| velocity_x | string | X velocity in m/s |
| velocity_y | string | Y velocity in m/s |
| velocity_z | string | Z velocity in m/s |
| attitude_w | string | Quaternion W (fixed, always 1) |
| attitude_x | string | Quaternion X (fixed, always 0) |
| attitude_y | string | Quaternion Y (fixed, always 0) |
| attitude_z | string | Quaternion Z (fixed, always 0) |
| mass | string | Mass in kg (default 420,000) |
| radius | string | Proximity envelope in meters (default 50) |
| parent_body | string | Reference body name (e.g., “Earth”) |
| system_id | string | Star system identifier (default “sol”) |
Station position/velocity fields use individual keys (not JSON arrays) unlike ships. Attitude is fixed at identity and never updated.
Game State Keys
game:tick
Current tick number as a string.
SET game:tick "123456"
GET game:tick # Returns "123456"
game:total_spawns
Total number of spawns ever (for spawn position calculation).
INCR game:total_spawns # Atomically increment and return new value
GET game:total_spawns # Returns current count
game:time
Current game time as ISO 8601 string. Updated each tick.
SET game:time "2000-01-01T13:00:00Z"
GET game:time # Returns "2000-01-01T13:00:00Z"
game:paused
Whether tick processing is paused.
SET game:paused "true"
SET game:paused "false"
game:tick_rate
Current tick rate in ticks per second.
SET game:tick_rate "1.0"
GET game:tick_rate # Returns "1.0"
Updated by tick-engine when admin changes tick rate. Used by api-gateway for welcome message.
game:time_scale
Current time scale (game speed multiplier).
SET game:time_scale "1.0"
GET game:time_scale # Returns "1.0"
Updated by tick-engine when admin changes time scale. Used by api-gateway for welcome message. Default 1.0 (real-time). Range 0.1 to 100.0.
game:time_sync
Whether game time synchronization to wall-clock UTC is enabled.
SET game:time_sync "true"
GET game:time_sync # Returns "true" or "false"
Default “true”. When enabled and admin time_scale ≈ 1.0, the tick engine adjusts effective time scale to converge game time to UTC.
game:time_drift
Current drift between game time and wall-clock UTC, in seconds. Positive means game is behind UTC.
SET game:time_drift "15.3"
GET game:time_drift # Returns drift in seconds
Updated each tick by tick-engine. Used by admin dashboard to display sync status.
game:registration_open
Whether new player registrations are allowed. Absent or “true” means open; “false” means closed.
SET game:registration_open "false"
GET game:registration_open # Returns "true", "false", or nil (treated as open)
Set by api-gateway admin endpoint. Checked during player registration.
Listing All Ships
To find all active ships, use SCAN (non-blocking cursor-based iteration):
SCAN 0 MATCH ship:* COUNT 100
Do not use KEYS ship:* — the KEYS command scans the entire keyspace and blocks the Redis server until complete. As the number of keys grows, this blocks all Redis operations for all services during the scan. Always use SCAN (or scan_iter() in the Python client) instead.
Listing All Bodies
SCAN 0 MATCH body:* COUNT 100
Listing All Stations
SCAN 0 MATCH station:* COUNT 100
System Index Sets
Per-system index sets enable efficient lookups of entities by star system.
Key: system:{system_id}:bodies (Set), system:{system_id}:ships (Set), system:{system_id}:stations (Set), system:{system_id}:jumpgates (Set)
Operations:
SADD system:sol:bodies "Earth" "Luna" "Mars" # Add bodies to system
SMEMBERS system:sol:ships # List all ships in Sol
SREM system:sol:ships {ship_id} # Remove ship from system index
Maintained by physics service on entity create/delete/clear. Rebuilt by tick-engine on snapshot restore.
Online Status Tracking
Key: connections:online
Tracks which players have active WebSocket connections.
Operations:
SADD connections:online {player_id} # Player connected
SREM connections:online {player_id} # Player disconnected
SISMEMBER connections:online {player_id} # Check if online
SMEMBERS connections:online # List all online players
SCARD connections:online # Count online players
Lifecycle:
| Event | Action |
|---|---|
| WebSocket connection established | SADD after token validated |
| WebSocket close (graceful) | SREM immediately |
| WebSocket close (abrupt) | SREM when detected via ping timeout |
| Player deleted by admin | SREM after WebSocket closed |
Disconnection detection:
| Method | Timeout | Action |
|---|---|---|
| Client ping | 30 seconds | Client sends ping |
| Server pong timeout | 10 seconds | Client should reconnect |
| Server-side heartbeat | 60 seconds | Server checks last message time |
| No activity timeout | 60 seconds | Server closes connection, SREM |
The 60-second server-side heartbeat ensures stale connections are cleaned up even if the client crashes without closing the WebSocket.
Last activity tracking:
The api-gateway tracks last message timestamp per connection in-memory (not in Redis):
| Data | Storage | Rationale |
|---|---|---|
| Last activity time | api-gateway memory | Per-connection, doesn’t need persistence |
| Online player set | Redis (connections:online) |
Shared across api-gateway instances |
On each incoming WebSocket message (including ping), api-gateway updates the in-memory timestamp for that connection. A background task checks all connections every 60 seconds and closes any with no activity.
If api-gateway restarts, all WebSocket connections are already closed (TCP termination), so in-memory tracking loss is not a problem—clients must reconnect anyway.
Fleet (Multi-Ship) Keys
player:{player_id}:active_ship
String key storing the UUID of the player’s currently active (piloted) ship.
SET player:{player_id}:active_ship "5995849b-..."
GET player:{player_id}:active_ship # Returns ship UUID string
Set by players service on ship switch and as fallback during login. Read during authentication to restore the player’s last-piloted ship.
Important: The players service Redis client does not use decode_responses=True, so reads return bytes. All consumers must decode bytes to str before comparing with UUID strings.
player:{player_id}:ships
Set of ship UUIDs belonging to a player. Rebuilt from DB on each login.
SADD player:{player_id}:ships "ship-uuid-1" "ship-uuid-2"
SMEMBERS player:{player_id}:ships
Data Ownership
| Data | Owner Service | Operations |
|---|---|---|
ship:* |
physics | Create, update, delete (api-gateway may write name field via ship rename) |
station:* |
physics | Create, update, delete |
body:* |
physics | Update (galaxy initializes) |
game:tick |
tick-engine | Read/write |
game:time |
tick-engine | Read/write |
game:total_spawns |
physics | Increment on spawn |
game:paused |
tick-engine | Read/write |
game:tick_rate |
tick-engine | Read/write |
game:time_scale |
tick-engine | Read/write |
game:time_sync |
tick-engine | Read/write |
game:time_drift |
tick-engine | Write (updated each tick) |
maneuver:* |
tick-engine | Create, update, delete (automation engine) |
automation:* |
api-gateway (create/update/delete), tick-engine (evaluate/disable) | Rule CRUD and evaluation |
game:registration_open |
api-gateway | Set by admin endpoint, read during registration |
connections:online |
api-gateway | Add/remove on connect/disconnect |
player:*:active_ship |
players | Set on login/switch, read during auth |
player:*:ships |
players | Rebuilt from DB on login |
system:*:bodies |
physics | Add on init/restore, clear on reset |
system:*:ships |
physics | Add on spawn, remove on delete, clear on reset |
system:*:stations |
physics | Add on spawn, remove on delete, clear on reset |
system:*:jumpgates |
physics | Add on spawn, remove on delete, clear on reset |
Cross-Service Read Access
Some services require read-only access to data owned by other services:
| Reader | Data | Purpose |
|---|---|---|
| players | connections:online |
Populate online field in ListPlayers response |
| api-gateway | player:*:active_ship |
Resolve active ship on WebSocket connect |
| api-gateway | game:tick, game:time, game:paused, game:tick_rate, game:time_scale, game:time_sync, game:time_drift, game:registration_open |
Build welcome message, /api/status, registration gate |
| tick-engine | All keys | Create snapshots |
Cross-service reads are acceptable for read-only access. Write operations must go through the owning service.
Persistence
Redis configured with AOF (Append Only File) for durability:
appendonly yes
appendfsync everysec
This provides durability with ~1 second of potential data loss on crash. Full state recovery uses PostgreSQL snapshots as the authoritative source.