WebSocket API

Real-time bidirectional protocol for game state streaming and ship control.

Transport

Parameter Value
Protocol WSS (WebSocket over TLS)
Endpoint wss://galaxy.example.com/ws
Development wss://localhost:30002/ws
Max message size 64 KB
Subprotocol None (raw JSON frames)

Connection Lifecycle

  1. Client opens WebSocket to /ws (no token in URL)
  2. Server validates Origin header (see Origin Validation below)
  3. Client starts a 10-second connection timeout
  4. On open, client sends auth message: {"type": "auth", "token": "<jwt>"}
  5. Server validates token; on failure closes with close code (see below)
  6. Server sends welcome with player info — client clears connection timeout
  7. Server immediately sends first state message with current game state
  8. Server sends state updates each tick (rate-limited to 10 Hz during catch-up)
  9. Client sends ping every 30 seconds; server responds with pong
  10. If no pong received within 10 seconds, client should reconnect

Tokens are never sent in URL query strings to avoid exposure in HTTP access logs, browser history, and proxy logs. The server closes the connection (code 4001) if no valid auth message arrives within 5 seconds of the WebSocket upgrade.

Origin Validation (CSRF Protection)

WebSocket connections bypass standard CORS protections. Before calling websocket.accept(), the server must validate the Origin header to prevent cross-site WebSocket hijacking (CSRF).

Validation rules:

Condition Action
No Origin header present Allow — non-browser clients (game clients, test tools) may not send Origin
Origin matches allowed origins Allow
Origin is a localhost variant Allow — development convenience (http://localhost:*, https://localhost:*)
Origin does not match Reject — close with code 4003 (“Origin not allowed”) before accepting

Allowed origins are derived from the existing cors_origins configuration setting (same origins used for HTTP CORS). The server parses the comma-separated cors_origins string into a set at startup.

localhost detection: Any origin where the hostname is localhost or 127.0.0.1 is allowed regardless of port or scheme. This covers development scenarios where the web client runs on varying ports.

Implementation: Check origin before websocket.accept(). If rejected, call websocket.close(code=4003, reason="Origin not allowed") without accepting the connection.

If the connection timeout fires (WebSocket does not reach OPEN state within 10 seconds), the client closes the socket and triggers the reconnection backoff sequence.

Ship-less Admin Mode

Admin accounts with ship_id = NULL (empty string in JWT) can connect via WebSocket for observation. The auth validation allows empty ship_id when the JWT’s is_admin claim is true.

Behavior:

  • Welcome message includes is_admin: true and ship_id: ""
  • State broadcasts include all bodies, ships, and stations, but ship field is null
  • Ship-dependent messages return error E030 “No ship assigned”:
    • control, service, attitude_hold, attitude_prograde, attitude_retrograde, attitude_normal, attitude_antinormal, attitude_radial, attitude_antiradial, attitude_local_horizontal, attitude_local_vertical, attitude_target, attitude_anti_target, attitude_target_prograde, attitude_target_retrograde, automation_create, automation_update, automation_delete, automation_list, maneuver_query, maneuver_abort, maneuver_pause, maneuver_resume, target_select, ship_rename
  • Non-ship messages work normally: ping, chat_send

Close Codes

Code Reason Client Action
4001 Authentication failed (generic; specific reason logged server-side) Display auth error, redirect to login (do not reconnect)
4003 Origin not allowed (CSRF protection) Display error, do not reconnect
4013 Server full Display “Server full, try again later”
1001 Server shutting down Reconnect with backoff

Server → Client Messages

welcome

Sent once on successful authentication.

{
  "type": "welcome",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "is_admin": false,
  "config": {
    "server_name": "Galaxy",
    "tick_rate": 1.0,
    "time_scale": 1.0,
    "game_time": "2025-01-15T10:30:00Z",
    "paused": false
  },
  "versions": {
    "api_gateway": "1.10.1",
    "physics": "1.10.1",
    "tick_engine": "1.10.1",
    "web_client": "1.10.1"
  }
}
Field Type Required Description
type string yes "welcome"
player_id uuid yes Player’s unique identifier
ship_id uuid yes Player’s ship unique identifier (empty string for ship-less admins)
is_admin boolean yes True if player has admin role
config object yes Current game configuration
config.server_name string yes Server instance name (from SERVER_NAME env var, default "Galaxy")
config.tick_rate number yes Current ticks per second (from Redis game:tick_rate)
config.time_scale number yes Game speed multiplier, 1.0 = real-time (from Redis game:time_scale)
config.game_time string yes Current game time, ISO 8601 (from Redis game:time)
config.paused boolean yes Whether tick processing is paused (from Redis game:paused)
versions object yes Service version strings (semver)
versions.api_gateway string yes API gateway version
versions.physics string yes Physics service version
versions.tick_engine string yes Tick engine version
versions.web_client string yes Web client version
system_id string yes Star system of the player’s active ship (default "sol")

Versions are fetched from each service’s /health/ready endpoint at api-gateway startup. The api-gateway’s own version comes from src/__init__.__version__.

If config.paused is true, the client should display “GAME PAUSED” overlay and expect no state updates until a game_resumed message arrives. Receipt of a state message implies the game is not paused; if the client’s pause state is stale (e.g., missed game_resumed during reconnect), it should self-correct.


state

Sent every tick with the full game state. During catch-up (server processing missed ticks), broadcasts are rate-limited to 10 Hz wall-clock.

{
  "type": "state",
  "tick": 123456,
  "game_time": "2025-01-15T10:30:00Z",
  "effective_time_scale": 1.0,
  "players_online": 3,
  "bodies": [
    {
      "name": "Earth",
      "type": "planet",
      "position": {"x": 1.496e11, "y": 0.0, "z": 0.0},
      "velocity": {"x": 0.0, "y": 29780.0, "z": 0.0},
      "mass": 5.972e24,
      "radius": 6371000.0,
      "rotation_period": 86164.1,
      "axial_tilt": 23.44,
      "prime_meridian_at_epoch": 0.0
    }
  ],
  "ship": {
    "ship_id": "660e8400-e29b-41d4-a716-446655440001",
    "player_id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "PlayerOne",
    "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,
    "inertia_tensor": {"x": 320000.0, "y": 320000.0, "z": 80000.0},
    "wheel_saturation": {"x": 0.0, "y": 0.0, "z": 0.0},
    "attitude_hold": false,
    "attitude_mode": "none",
    "ship_class": "fast_frigate"
  },
  "ships": [
    {
      "ship_id": "770e8400-e29b-41d4-a716-446655440002",
      "player_id": "880e8400-e29b-41d4-a716-446655440003",
      "name": "OtherPlayer",
      "position": {"x": 1.496e11, "y": 100000.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,
      "inertia_tensor": {"x": 320000.0, "y": 320000.0, "z": 80000.0},
      "ship_class": "fast_frigate"
    }
  ],
  "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"
    }
  ]
}

Top-level fields:

Field Type Required Description
type string yes "state"
tick integer yes Current tick number
game_time string yes Current game time (ISO 8601)
effective_time_scale number yes Actual time scale applied (may differ from admin-set scale during time sync)
players_online integer yes Number of connected players (from Redis SCARD connections:online)
bodies array yes All celestial bodies (always 31 elements)
ship object|null yes Player’s own ship (null if ship does not exist)
ships array yes All other players’ ships (empty array if none)
stations array yes All stations (empty array if none)

Body fields:

Field Type Required Description
name string yes Body name (e.g., “Earth”, “Luna”)
type string yes Body type: “star”, “planet”, or “moon”
position vec3 yes Position in ICRF (meters)
velocity vec3 yes Velocity in ICRF (m/s)
mass number yes Mass (kg)
radius number yes Mean radius (meters)
rotation_period number yes Sidereal rotation period (seconds, negative = retrograde)
axial_tilt number yes Obliquity (degrees)
prime_meridian_at_epoch number yes Prime meridian angle at J2000 epoch (degrees)

Own ship fields (includes private state):

Field Type Required Description
ship_id uuid yes Ship unique identifier
player_id uuid yes Owning player’s identifier
name string yes Player display name
position vec3 yes Position in ICRF (meters)
velocity vec3 yes Velocity in ICRF (m/s)
attitude quaternion yes Orientation quaternion (ICRF)
angular_velocity vec3 yes Angular velocity (rad/s, body frame)
thrust_level number yes Current throttle (0.0–1.0)
fuel number yes Remaining propellant (kg)
fuel_capacity number yes Maximum propellant capacity (kg)
mass number yes Total mass including fuel (kg)
inertia_tensor vec3 yes Diagonal inertia tensor (kg·m²)
wheel_saturation vec3 yes Reaction wheel saturation per axis (0.0–1.0)
attitude_hold boolean yes Whether attitude hold is enabled
attitude_mode string yes Current attitude mode (see values below)

Attitude mode values: "none", "hold", "prograde", "retrograde", "normal", "antinormal", "radial", "antiradial", "local_horizontal", "local_vertical", "target", "anti_target"

Other ship fields (excludes private state):

Same as own ship fields except wheel_saturation, attitude_hold, and attitude_mode are omitted. These are private internal state not shared with other players.

Station fields:

Field Type Required Description
station_id uuid yes Station unique identifier
name string yes Station display name
position vec3 yes Position in ICRF (meters)
velocity vec3 yes Velocity in ICRF (m/s)
attitude quaternion yes Fixed orientation quaternion (always identity)
mass number yes Station mass (kg)
radius number yes Proximity envelope radius (meters)
parent_body string yes Reference body name (e.g., “Earth”)

Notes:

  • bodies, ships, and stations arrays are always present (never omitted)
  • When no other players are online, ships is an empty array
  • wheel_saturation is computed by api-gateway from wheel_momentum: abs(wheel_momentum[axis]) / 10000
  • mass and fuel_capacity are computed by the physics service
  • Body mass is included in state messages (unlike the old spec which omitted it)

player_joined

Broadcast to all connected clients when another player connects.

{
  "type": "player_joined",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "name": "NewPlayer"
}
Field Type Required Description
type string yes "player_joined"
player_id uuid yes Joining player’s identifier
ship_id uuid yes Joining player’s ship identifier
name string yes Player display name (from JWT username claim)

This indicates connection status, not ship creation. A player’s ship continues to exist in the simulation even when disconnected.


player_left

Broadcast to all connected clients when another player disconnects.

{
  "type": "player_left",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "player_left"
player_id uuid yes Departing player’s identifier
ship_id uuid yes Departing player’s ship identifier

This indicates disconnection, not ship removal. The player’s ship continues to exist and move in the simulation. Ship removal only occurs when the player deletes their account, an admin deletes the player, or an admin performs a factory reset.


game_paused

Broadcast when an admin pauses the game.

{
  "type": "game_paused",
  "paused_at_tick": 123456
}
Field Type Required Description
type string yes "game_paused"
paused_at_tick integer yes Tick number at which the game was paused

Trigger: tick-engine publishes tick.paused event on the galaxy:tick Redis stream; api-gateway broadcasts to all WebSocket clients.

Client should display “GAME PAUSED” overlay and expect no further state updates until game_resumed.


game_resumed

Broadcast when an admin resumes the game.

{
  "type": "game_resumed",
  "resumed_at_tick": 123456
}
Field Type Required Description
type string yes "game_resumed"
resumed_at_tick integer yes Tick number at which the game was resumed

Trigger: tick-engine publishes tick.resumed event; api-gateway broadcasts to all WebSocket clients.

Client should hide “GAME PAUSED” overlay and expect state updates to resume.


game_restored

Broadcast when an admin restores from a snapshot.

{
  "type": "game_restored",
  "restored_to_tick": 123456,
  "game_time": "2025-01-15T10:30:00Z"
}
Field Type Required Description
type string yes "game_restored"
restored_to_tick integer yes Tick number of the restored snapshot
game_time string yes Game time at the restored snapshot (ISO 8601)

Trigger: tick-engine publishes tick.restored event; api-gateway broadcasts to all WebSocket clients.

Status: Specified but not yet implemented in api-gateway event loop. The tick.restored event is published by tick-engine but not currently forwarded to WebSocket clients.

Client behavior (when implemented):

  • Display notification: “Game state restored to [game_time]”
  • Expect ship positions may have changed dramatically
  • Next state message will contain the restored positions
  • Players whose ships did not exist in the restored snapshot will receive E006 on their next control command

tick_rate_changed

Broadcast when an admin changes the tick rate.

{
  "type": "tick_rate_changed",
  "previous_rate": 1.0,
  "new_rate": 2.0
}
Field Type Required Description
type string yes "tick_rate_changed"
previous_rate number yes Previous tick rate (Hz)
new_rate number yes New tick rate (Hz)

Trigger: tick-engine publishes tick.rate_changed event; api-gateway broadcasts to all WebSocket clients.


time_scale_changed

Broadcast when an admin changes the time scale.

{
  "type": "time_scale_changed",
  "previous_scale": 1.0,
  "new_scale": 10.0
}
Field Type Required Description
type string yes "time_scale_changed"
previous_scale number yes Previous time scale multiplier
new_scale number yes New time scale multiplier

Trigger: tick-engine publishes tick.time_scale_changed event; api-gateway broadcasts to all WebSocket clients.


ship.spawned

Broadcast when a new ship is created (e.g., player registration or reset service).

{
  "type": "ship.spawned",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "PlayerOne",
  "position": {"x": 1.496e11, "y": 0.0, "z": 6771000.0}
}
Field Type Required Description
type string yes "ship.spawned"
ship_id uuid yes New ship’s identifier
player_id uuid yes Owning player’s identifier
name string yes Ship display name (defaults to player username on spawn)
position vec3 yes Initial spawn position (meters, ICRF)

Trigger: physics service publishes ship.spawned event on the galaxy:ships Redis stream; api-gateway broadcasts to all WebSocket clients.


ship.removed

Broadcast when a ship is removed (e.g., account deletion).

{
  "type": "ship.removed",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "player_id": "550e8400-e29b-41d4-a716-446655440000"
}
Field Type Required Description
type string yes "ship.removed"
ship_id uuid yes Removed ship’s identifier
player_id uuid yes Owning player’s identifier

Trigger: physics service publishes ship.removed event on the galaxy:ships Redis stream; api-gateway broadcasts to all WebSocket clients.


ship.destroyed

Broadcast when a ship collides with a celestial body and is permanently destroyed. The ship is removed from Redis, the player’s fleet DB, and all client views. If it was the player’s active ship, the server automatically switches to another ship (if available).

{
  "type": "ship.destroyed",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "body": "Earth",
  "reason": "collision"
}
Field Type Required Description
type string yes "ship.destroyed"
ship_id uuid yes Destroyed ship’s identifier
player_id uuid yes Owning player’s identifier
body string yes Name of body the ship crashed into
reason string yes Destruction cause ("collision", "combat", etc.)

Trigger: physics service detects dist(ship, body) < body.radius during tick processing, deletes the ship from Redis, publishes ship.destroyed event on the galaxy:ships Redis stream; api-gateway broadcasts to all WebSocket clients and notifies the players service to remove the ship from the fleet database.


station.spawned

Broadcast when a new station is created (admin action).

{
  "type": "station.spawned",
  "station_id": "990e8400-e29b-41d4-a716-446655440004",
  "name": "Gateway Station",
  "parent_body": "Earth"
}
Field Type Required Description
type string yes "station.spawned"
station_id uuid yes New station’s identifier
name string yes Station display name
parent_body string yes Reference body name

Trigger: physics service publishes station.spawned event on the galaxy:stations Redis stream; api-gateway broadcasts to all WebSocket clients.


station.removed

Broadcast when a station is removed (admin action).

{
  "type": "station.removed",
  "station_id": "990e8400-e29b-41d4-a716-446655440004"
}
Field Type Required Description
type string yes "station.removed"
station_id uuid yes Removed station’s identifier

Trigger: physics service publishes station.removed event on the galaxy:stations Redis stream; api-gateway broadcasts to all WebSocket clients.


jumpgate.spawned

Broadcast when a new jump gate is created (admin action).

{
  "type": "jumpgate.spawned",
  "jumpgate_id": "aa0e8400-e29b-41d4-a716-446655440005",
  "name": "Earth-Luna L4 Gate",
  "parent_body": "Earth"
}
Field Type Required Description
type string yes "jumpgate.spawned"
jumpgate_id uuid yes New jump gate’s identifier
name string yes Jump gate display name
parent_body string yes Reference body name

Trigger: physics service publishes jumpgate.spawned event on the galaxy:jumpgates Redis stream; api-gateway broadcasts to all WebSocket clients.


jumpgate.removed

Broadcast when a jump gate is removed (admin action).

{
  "type": "jumpgate.removed",
  "jumpgate_id": "aa0e8400-e29b-41d4-a716-446655440005"
}
Field Type Required Description
type string yes "jumpgate.removed"
jumpgate_id uuid yes Removed jump gate’s identifier

Trigger: physics service publishes jumpgate.removed event on the galaxy:jumpgates Redis stream; api-gateway broadcasts to all WebSocket clients.


versions_updated

Broadcast when service versions change (detected by polling every 60 seconds).

{
  "type": "versions_updated",
  "versions": {
    "api_gateway": "1.10.1",
    "physics": "1.10.1",
    "tick_engine": "1.10.1",
    "web_client": "1.10.1"
  }
}
Field Type Required Description
type string yes "versions_updated"
versions object yes Updated service versions (same structure as welcome.versions)

Trigger: api-gateway polls /health/ready on physics, tick-engine, and /version.json on web-client every 60 seconds. If any version changes, this message is broadcast.

Internal transport: Version polling uses cluster-internal plain HTTP (not HTTPS). TLS terminates at the ingress/NodePort for external traffic; pod-to-pod calls within the cluster use HTTP.


chat_message

Broadcast to all connected clients when a player sends a chat message.

{
  "type": "chat_message",
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "PlayerOne",
  "message": "Hello everyone!",
  "timestamp": 1705315800
}
Field Type Required Description
type string yes "chat_message"
player_id uuid yes Sender’s player identifier
username string yes Sender’s display name
message string yes Message text (1–256 characters, trimmed)
timestamp number yes Server Unix time in seconds (epoch)

Trigger: A connected client sends a valid chat_send message. The server broadcasts to ALL connected clients (including the sender).


targeted_by

Sent to a player when other players target or untarget their ship. Contains the full list of current targeters (not incremental). An empty targeters array means no one is targeting the ship.

{
  "type": "targeted_by",
  "targeters": [
    {
      "player_id": "550e8400-e29b-41d4-a716-446655440000",
      "ship_id": "660e8400-e29b-41d4-a716-446655440001",
      "name": "PlayerOne"
    }
  ]
}
Field Type Required Description
type string yes "targeted_by"
targeters array yes List of players currently targeting this ship (empty = no one)
targeters[].player_id uuid yes Targeting player’s identifier
targeters[].ship_id uuid yes Targeting player’s ship identifier
targeters[].name string yes Targeting player’s display name

Trigger: Another player sends a target_select message selecting or deselecting a ship target. The server maintains in-memory targeting state and sends the full targeter list whenever it changes for a given ship.

Edge cases:

  • Targeter disconnects → removed from targeting state, target ship notified with updated list
  • Target ship reconnects → receives current targeter list on connection
  • Multiple targeters → all included in the array

pong

Response to a client ping.

{
  "type": "pong",
  "timestamp": 1705315800000
}
Field Type Required Description
type string yes "pong"
timestamp integer yes Echoed timestamp from the client’s ping (Unix milliseconds)

Used for round-trip latency measurement: RTT = Date.now() - pong.timestamp.


error

Sent when the server encounters an error processing a client message.

{
  "type": "error",
  "code": "E001",
  "message": "Invalid rotation value"
}
Field Type Required Description
type string yes "error"
code string yes Error code (see Error Codes)
message string yes Human-readable error description

service_error

Sent when a ship service request (fuel, reset, dock, undock) fails at the physics layer (distinct from a gRPC transport error, which returns error).

{
  "type": "service_error",
  "service": "dock",
  "code": "E031"
}
Field Type Required Description
type string yes "service_error"
service string yes Service that failed ("fuel", "reset", "dock", "undock")
code string yes Error code from physics response

Trigger: Client sends service message; physics gRPC RequestService returns success=false.


automation_created

Confirmation that an automation rule was created.

{
  "type": "automation_created",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rule": {
    "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
    "name": "Circularize",
    "enabled": true,
    "mode": "once",
    "priority": 50,
    "trigger": {"conditions": [{"field": "eccentricity", "op": ">", "value": 0.01}], "logic": "AND"},
    "actions": [{"action": "orbit_match", "ref_body": "Earth", "target_eccentricity": 0.0}],
    "created_at": "2025-01-15T10:30:00+00:00"
  }
}
Field Type Required Description
type string yes "automation_created"
ship_id uuid yes Ship the rule belongs to
rule object yes Full rule object (see rule fields below)

Rule fields: rule_id (uuid), name (string), enabled (bool), mode ("once" or "continuous"), priority (int 0–99), trigger (object), actions (array), created_at (ISO 8601).

Trigger: Client sends automation_create. Only sent to requesting client.


automation_updated

Confirmation that an automation rule was updated.

{
  "type": "automation_updated",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rule": {
    "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
    "name": "Circularize (updated)",
    "enabled": true,
    "mode": "continuous",
    "priority": 50,
    "trigger": {"conditions": [{"field": "eccentricity", "op": ">", "value": 0.01}], "logic": "AND"},
    "actions": [{"action": "orbit_match", "ref_body": "Earth", "target_eccentricity": 0.0}],
    "created_at": "2025-01-15T10:30:00+00:00"
  }
}
Field Type Required Description
type string yes "automation_updated"
ship_id uuid yes Ship the rule belongs to
rule object yes Full rule object with updated fields

Trigger: Client sends automation_update. Only sent to requesting client.


automation_deleted

Confirmation that an automation rule was deleted.

{
  "type": "automation_deleted",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rule_id": "bb0e8400-e29b-41d4-a716-446655440010"
}
Field Type Required Description
type string yes "automation_deleted"
ship_id uuid yes Ship the rule belonged to
rule_id uuid yes Deleted rule’s identifier

Trigger: Client sends automation_delete. Only sent to requesting client.


automation_rules

List of all automation rules for a ship, sorted by priority.

{
  "type": "automation_rules",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rules": [
    {
      "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
      "name": "Circularize",
      "enabled": true,
      "mode": "once",
      "priority": 50,
      "trigger": {"conditions": [], "logic": "AND"},
      "actions": [],
      "created_at": "2025-01-15T10:30:00+00:00"
    }
  ]
}
Field Type Required Description
type string yes "automation_rules"
ship_id uuid yes Ship the rules belong to
rules array yes Array of rule objects, sorted by priority ascending

Trigger: Client sends automation_list. Only sent to requesting client.


automation_triggered

Notification that an automation rule fired during tick processing.

{
  "type": "automation_triggered",
  "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
  "rule_name": "Circularize",
  "tick": 123456,
  "actions_executed": [{"action": "orbit_match", "ref_body": "Earth"}],
  "ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "ship_name": "Horse"
}
Field Type Required Description
type string yes "automation_triggered"
rule_id uuid yes Rule that fired
rule_name string yes Rule display name
tick integer yes Tick when the rule fired
actions_executed array yes Actions that were executed
ship_id uuid yes Ship the rule belongs to
ship_name string yes Ship display name

Trigger: tick-engine publishes automation.triggered event on the galaxy:events Redis stream; api-gateway forwards to the owning player (looked up via Redis ship:{id}player_id). Only sent to the ship’s owner, not broadcast.


maneuver_status

Current maneuver state for the player’s ship.

{
  "type": "maneuver_status",
  "active": true,
  "maneuver": {
    "type": "rendezvous",
    "ref_body": "Earth",
    "rule_name": "Dock at Station",
    "started_tick": 100,
    "started_game_time": "2025-01-15T10:30:00+00:00",
    "phase": "plane_change",
    "sub_phase": "burning",
    "target_inclination": 0.5,
    "target_id": "990e8400-e29b-41d4-a716-446655440004",
    "target_type": "station",
    "dock_on_arrival": true,
    "status_text": "Plane change: 2.3° remaining",
    "element_errors": {"inclination": 0.04, "raan": 0.01},
    "burn_eta_seconds": 120,
    "paused": false,
    "brach_sub_phase": null,
    "brach_coast_ratio": null,
    "brach_burn_time": null,
    "brach_coast_time": null,
    "brach_total_time": null,
    "brach_elapsed": null,
    "hold_distance_m": null
  }
}
Field Type Required Description
type string yes "maneuver_status"
active boolean yes Whether a maneuver is running
maneuver object|null yes Maneuver details (null if active is false)

Maneuver object fields:

Field Type Description
type string Maneuver type (e.g., "orbit_match", "rendezvous", "station_keep")
ref_body string Reference body name
rule_name string Automation rule that started the maneuver
started_tick integer Tick when maneuver began
started_game_time string|null ISO 8601 game time when started
phase string|null Current maneuver phase
sub_phase string|null Current sub-phase
target_inclination number|null Target inclination (radians)
target_id uuid|null Target entity ID
target_type string|null Target entity type
dock_on_arrival boolean|null Whether to dock when close enough
status_text string|null Human-readable status
element_errors object|null Orbital element error magnitudes
burn_eta_seconds integer|null Estimated burn time remaining
paused boolean Whether the maneuver is paused
brach_sub_phase string|null Brachistochrone sub-phase
brach_coast_ratio number|null Brachistochrone coast ratio
brach_burn_time number|null Brachistochrone burn time (seconds)
brach_coast_time number|null Brachistochrone coast time (seconds)
brach_total_time number|null Brachistochrone total time (seconds)
brach_elapsed number|null Brachistochrone elapsed time (seconds)
hold_distance_m number|null Station-keeping hold distance (meters)

Trigger: Client sends maneuver_query. Only sent to requesting client.


maneuver_aborted

Confirmation that a maneuver was aborted.

{
  "type": "maneuver_aborted",
  "maneuver_type": "rendezvous"
}
Field Type Required Description
type string yes "maneuver_aborted"
maneuver_type string yes Type of maneuver that was aborted

Trigger: Client sends maneuver_abort while a maneuver is active. Errors: E028 if no active maneuver.


maneuver_paused

Confirmation that a maneuver was paused.

{
  "type": "maneuver_paused",
  "maneuver_type": "orbit_match"
}
Field Type Required Description
type string yes "maneuver_paused"
maneuver_type string yes Type of maneuver that was paused

Trigger: Client sends maneuver_pause. Errors: E028 if no active maneuver, E029 if already paused.


maneuver_resumed

Confirmation that a paused maneuver was resumed.

{
  "type": "maneuver_resumed",
  "maneuver_type": "orbit_match"
}
Field Type Required Description
type string yes "maneuver_resumed"
maneuver_type string yes Type of maneuver that was resumed

Trigger: Client sends maneuver_resume. Errors: E028 if no active maneuver, E030 if not paused.


ship_switched

Confirmation that the player’s active ship was switched.

{
  "type": "ship_switched",
  "ship_id": "660e8400-e29b-41d4-a716-446655440002"
}
Field Type Required Description
type string yes "ship_switched"
ship_id uuid yes New active ship ID

Trigger: Client sends switch_ship and the players service confirms the switch. Only sent to requesting client. Subsequent state broadcasts reflect the new active ship.


fleet_list

List of all ships owned by the player, enriched with live Redis state.

{
  "type": "fleet_list",
  "ships": [
    {
      "ship_id": "660e8400-e29b-41d4-a716-446655440001",
      "name": "Horse",
      "ship_class": "fast_frigate",
      "fuel": 10000.0,
      "fuel_capacity": 10000.0,
      "thrust_level": 0.0,
      "docked_station_id": "",
      "position": {"x": 1.496e11, "y": 0.0, "z": 6771000.0},
      "rule_count": 2
    }
  ],
  "active_ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "fleet_list"
ships array yes Array of ship objects with live state
active_ship_id uuid yes Currently active ship ID

Ship object fields: ship_id, name, ship_class, fuel, fuel_capacity, thrust_level, docked_station_id, position (vec3, if available), rule_count (number of automation rules), system_id (star system, default "sol").

Trigger: Client sends fleet_list. Only sent to requesting client.


fleet_spawned

Confirmation that a new fleet ship was spawned.

{
  "type": "fleet_spawned",
  "ship_id": "660e8400-e29b-41d4-a716-446655440002",
  "name": "Scout",
  "ship_class": "fast_frigate"
}
Field Type Required Description
type string yes "fleet_spawned"
ship_id uuid yes New ship’s identifier
name string yes Ship display name
ship_class string yes Ship class type

Trigger: Client sends fleet_spawn and the players service successfully creates the ship. Only sent to requesting client.


fleet_abandoned

Confirmation that a fleet ship was abandoned.

{
  "type": "fleet_abandoned",
  "ship_id": "660e8400-e29b-41d4-a716-446655440002",
  "active_ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "fleet_abandoned"
ship_id uuid yes Abandoned ship’s identifier
active_ship_id uuid yes Current active ship (may change if the abandoned ship was active)

Trigger: Client sends fleet_abandon and the players service confirms. Only sent to requesting client.


fleet_command_result

Result of a fleet-wide command issued to all non-active ships.

{
  "type": "fleet_command_result",
  "command": "follow_me",
  "ships_affected": 3
}
Field Type Required Description
type string yes "fleet_command_result"
command string yes Command that was executed ("follow_me" or "hold_position")
ships_affected integer yes Number of fleet ships that received the command

Trigger: Client sends fleet_command. follow_me creates station-keeping automation rules on each fleet ship targeting the active ship. hold_position deletes all automation rules and sets thrust to 0. Only sent to requesting client.


galaxy:systems

List of all star systems in the galaxy.

{
  "type": "galaxy:systems",
  "systems": [
    {
      "id": "sol",
      "name": "Sol",
      "star_body": "Sun",
      "position": {"x": 0.0, "y": 0.0, "z": 0.0}
    }
  ]
}
Field Type Required Description
type string yes "galaxy:systems"
systems array yes Array of star system objects
systems[].id string yes System identifier (e.g., "sol")
systems[].name string yes Display name (e.g., "Sol")
systems[].star_body string yes Name of the primary star body
systems[].position vec3 yes Galactic position in meters

Trigger: Client sends request_systems. Only sent to requesting client.


automation_copied

Confirmation that automation rules were copied between ships.

{
  "type": "automation_copied",
  "source_ship_id": "550e8400-e29b-41d4-a716-446655440000",
  "target_ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rules": [
    {
      "rule_id": "new-uuid",
      "name": "Rule name",
      "enabled": false,
      "mode": "once",
      "priority": 50,
      "trigger": {},
      "actions": [],
      "created_at": "2025-01-15T10:30:00+00:00"
    }
  ],
  "count": 1
}
Field Type Required Description
type string yes "automation_copied"
source_ship_id uuid yes Ship rules were copied from
target_ship_id uuid yes Ship rules were copied to
rules array yes Array of newly created rule objects on target ship
count integer yes Number of rules copied

Notes: Copied rules are always created with enabled: false to prevent unintended activation. Name conflicts are resolved by appending " (N)" suffix. Only sent to requesting client.


system_snapshot

Full snapshot of all entities in a star system, returned in response to request_system_snapshot.

{
  "type": "system_snapshot",
  "system_id": "sol",
  "bodies": [...],
  "ships": [...],
  "stations": [...],
  "jumpgates": [...]
}
Field Type Required Description
type string yes "system_snapshot"
system_id string yes Requested system identifier
bodies array yes All celestial bodies in the system (same format as state message bodies)
ships array yes All ships in the system (same format as state message ships)
stations array yes All stations in the system (same format as state message stations)
jumpgates array yes All jump gates in the system

Trigger: Client sends request_system_snapshot. Only sent to requesting client.


ship_log_history

Ship event log entries returned in response to ship_log_request.

{
  "type": "ship_log_history",
  "ship_id": "550e8400-e29b-41d4-a716-446655440000",
  "entries": [
    {
      "id": 42,
      "ship_id": "550e8400-e29b-41d4-a716-446655440000",
      "event_type": "spawn",
      "game_time": "2025-01-15T10:30:00+00:00",
      "tick": 12345,
      "message": "Ship spawned at ISS"
    }
  ],
  "has_more": true
}
Field Type Required Description
type string yes "ship_log_history"
ship_id uuid yes Ship whose log was requested
entries array yes Array of log entry objects
entries[].id integer yes Log entry ID (for pagination cursor)
entries[].ship_id uuid yes Ship ID
entries[].event_type string yes Event type (e.g., "spawn", "dock", "crash")
entries[].game_time string yes Game time when event occurred (ISO 8601)
entries[].tick integer yes Tick number when event occurred
entries[].message string yes Human-readable event description
has_more boolean yes True if more entries exist before before_id cursor

Trigger: Client sends ship_log_request. Only sent to requesting client.


Client → Server Messages

auth

First message after WebSocket open. Must arrive within 5 seconds.

{
  "type": "auth",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Field Type Required Description
type string yes "auth"
token string yes JWT token from login or registration

control

Combined rotation and thrust input.

{
  "type": "control",
  "rotation": {"x": 1.0, "y": 0.0, "z": 0.0},
  "thrust_level": 0.5
}
Field Type Required Description
type string yes "control"
rotation vec3 no Rotation input per axis: pitch (x), yaw (y), roll (z). Each -1.0 to 1.0
thrust_level number no Throttle level, 0.0 to 1.0

Both fields are optional; omit either to leave it unchanged. Either or both can be sent in a single message.

Rate limit: 60 messages per second per connection. Within each tick, multiple commands are coalesced — only the most recent value is applied when physics processes.

Rotation input mapping:

Index Name Key (+1) Key (-1) Effect of +1
x Pitch W S Nose up
y Yaw A D Nose left
z Roll Q E Roll left (port wing drops)

Thrust behavior:

  • Thrust level persists until changed (set-and-forget)
  • Incremented/decremented by 0.1 per key press (+/- keys)
  • Clamped to 0.0–1.0

service

Request a game service.

{
  "type": "service",
  "service": "fuel",
  "body": "Earth"
}
Field Type Required Description
type string yes "service"
service string yes Service name: "fuel" or "reset"
body string no Target body for reset service (omit for fuel or Earth-default reset)

Rate limit: 1 request per minute per connection.

Response behavior:

  • On success: no response sent (fire-and-forget)
  • On error: server sends error message (E003, E004, E006, E007, E008)
  • Effect visible in next state message (fuel refilled, position reset)

attitude_hold

Enable or disable attitude hold (rate damping).

{
  "type": "attitude_hold",
  "enabled": true
}
Field Type Required Description
type string yes "attitude_hold"
enabled boolean yes true to enable hold mode, false to disable all attitude control

attitude_prograde

Orient ship nose toward velocity vector. No payload.

{
  "type": "attitude_prograde"
}

attitude_retrograde

Orient ship nose opposite velocity vector. No payload.

{
  "type": "attitude_retrograde"
}

attitude_normal

Orient ship nose along orbit normal (r × v). No payload.

{
  "type": "attitude_normal"
}

attitude_antinormal

Orient ship nose opposite orbit normal. No payload.

{
  "type": "attitude_antinormal"
}

attitude_radial

Orient ship nose perpendicular to velocity in orbital plane. No payload.

{
  "type": "attitude_radial"
}

attitude_antiradial

Orient ship nose opposite radial direction. No payload.

{
  "type": "attitude_antiradial"
}

attitude_local_horizontal

Orient ship nose along horizontal velocity component. No payload.

{
  "type": "attitude_local_horizontal"
}

attitude_local_vertical

Orient ship nose along radial direction from body center. No payload.

{
  "type": "attitude_local_vertical"
}

attitude_target

Orient ship nose toward a selected target entity. Requires target ID and type.

{
  "type": "attitude_target",
  "target_id": "Earth",
  "target_type": "body"
}
Field Type Required Description
type string yes "attitude_target"
target_id string yes Target entity ID (body name, ship_id, station_id)
target_type string yes "body", "ship", "station"

Gateway routes to physics SetAttitudeMode with ATTITUDE_TARGET mode and target fields. On failure (E022 target not found, E024 too close), sends error message to client.


attitude_anti_target

Orient ship nose away from a selected target entity. Requires target ID and type.

{
  "type": "attitude_anti_target",
  "target_id": "Earth",
  "target_type": "body"
}
Field Type Required Description
type string yes "attitude_anti_target"
target_id string yes Target entity ID (body name, ship_id, station_id)
target_type string yes "body", "ship", "station"

Gateway routes to physics SetAttitudeMode with ATTITUDE_ANTI_TARGET mode and target fields. On failure (E022 target not found, E024 too close), sends error message to client.


attitude_target_prograde

Orient ship nose along the relative velocity vector with respect to a selected target entity. Requires target ID and type.

{
  "type": "attitude_target_prograde",
  "target_id": "Earth",
  "target_type": "body"
}
Field Type Required Description
type string yes "attitude_target_prograde"
target_id string yes Target entity ID (body name, ship_id, station_id)
target_type string yes "body", "ship", "station"

Gateway routes to physics SetAttitudeMode with ATTITUDE_TARGET_PROGRADE mode and target fields. On failure (E022 target not found, E023 relative velocity too low), sends error message to client.


attitude_target_retrograde

Orient ship nose opposite to the relative velocity vector with respect to a selected target entity. Requires target ID and type.

{
  "type": "attitude_target_retrograde",
  "target_id": "Earth",
  "target_type": "body"
}
Field Type Required Description
type string yes "attitude_target_retrograde"
target_id string yes Target entity ID (body name, ship_id, station_id)
target_type string yes "body", "ship", "station"

Gateway routes to physics SetAttitudeMode with ATTITUDE_TARGET_RETROGRADE mode and target fields. On failure (E022 target not found, E023 relative velocity too low), sends error message to client.


chat_send

Send a chat message to the global channel.

{
  "type": "chat_send",
  "message": "Hello everyone!"
}
Field Type Required Description
type string yes "chat_send"
message string yes Message text (1–256 characters after trimming)

Validation:

  • Message must be a string
  • After .trim(), must be non-empty (error E020)
  • After .trim(), must be ≤ 256 characters (error E019)

Rate limit: 5 messages per second per player (error E018).

On success: Server broadcasts chat_message to all connected clients (including sender). No explicit acknowledgment is sent.


target_select

Inform the server of the player’s current target selection. Used to relay targeting information to other players (ship-to-ship only).

{
  "type": "target_select",
  "target_id": "660e8400-e29b-41d4-a716-446655440001",
  "target_type": "ship"
}
Field Type Required Description
type string yes "target_select"
target_id string|null yes Target entity ID (body name, ship_id, station_id, lagrange key), or null to deselect
target_type string|null yes "body", "ship", "station", "lagrange", or null to deselect

Server behavior:

  • Stores targeting state in-memory: player_id → (target_id, target_type)
  • Only ship-to-ship targeting triggers notification: if the old or new target is a ship, the server sends targeted_by to the affected ship’s owner
  • Non-ship targets (bodies, stations, lagrange points) are stored but do not trigger notifications (these aren’t players)
  • On disconnect, the player’s targeting entry is removed and any targeted ship is notified

No response sent — fire-and-forget. Effect is visible via targeted_by messages to other players.

Multi-target note: The client maintains an N-target set locally (state.targets Map). Only the focused target is sent to the server via target_select. The multi-target set is entirely client-local state — no protocol format change is needed.


ship_rename

Rename the player’s ship. The name is stored in the ship:{id} Redis hash and included in all subsequent state broadcasts to all players.

{
  "type": "ship_rename",
  "name": "My Ship"
}
Field Type Required Description
type string yes "ship_rename"
name string yes New ship name (3-32 characters after trimming whitespace)

Validation:

  • Name is trimmed of leading/trailing whitespace
  • After trimming, must be 3-32 characters (error E030 if invalid)

On success: Server updates ship:{id} Redis hash name field and sends ship_renamed confirmation to the requesting client. The new name is visible to all players in subsequent state broadcasts.

Default name: Ships are created with the player’s username as the default name (set during spawn).


ship_renamed

Server response confirming a successful ship rename.

{
  "type": "ship_renamed",
  "name": "My Ship"
}
Field Type Required Description
type string yes "ship_renamed"
name string yes The new ship name (as stored)

Trigger: Client sends a valid ship_rename message. Only sent to the requesting client (not broadcast).


ping

Heartbeat / latency measurement.

{
  "type": "ping",
  "timestamp": 1705315800000
}
Field Type Required Description
type string yes "ping"
timestamp integer yes Client timestamp (Unix milliseconds via Date.now())

Sent every 30 seconds. Server echoes as pong with the same timestamp.


automation_create

Create a new automation rule for a ship.

{
  "type": "automation_create",
  "name": "Circularize",
  "mode": "once",
  "priority": 50,
  "trigger": {
    "conditions": [{"field": "eccentricity", "op": ">", "value": 0.01}],
    "logic": "AND"
  },
  "actions": [{"action": "orbit_match", "ref_body": "Earth", "target_eccentricity": 0.0}],
  "ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "automation_create"
name string yes Rule display name (1–64 characters)
mode string no "once" (default) or "continuous"
priority integer no 0–99, default 50 (lower = higher priority)
trigger object yes Trigger conditions
actions array yes Actions to execute
ship_id uuid no Target ship (defaults to active ship; must be owned)

Validation errors: E026 for invalid fields, E025 if rule limit reached (max 10 per ship), E034 if ship not owned.

On success: Server responds with automation_created.


automation_update

Update an existing automation rule.

{
  "type": "automation_update",
  "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
  "name": "Circularize (updated)",
  "enabled": false,
  "ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "automation_update"
rule_id uuid yes Rule to update
name string no New name (1–64 characters)
enabled boolean no Enable/disable the rule
mode string no "once" or "continuous"
priority integer no 0–99
trigger object no New trigger conditions
actions array no New actions
ship_id uuid no Target ship (defaults to active ship; must be owned)

Only provided fields are updated; omitted fields remain unchanged.

Validation errors: E027 if rule not found, E026 for invalid fields, E034 if ship not owned.

On success: Server responds with automation_updated.


automation_delete

Delete an automation rule.

{
  "type": "automation_delete",
  "rule_id": "bb0e8400-e29b-41d4-a716-446655440010",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "automation_delete"
rule_id uuid yes Rule to delete
ship_id uuid no Target ship (defaults to active ship; must be owned)

Validation errors: E027 if rule not found or rule_id missing, E034 if ship not owned.

On success: Server responds with automation_deleted.


automation_list

Request the list of all automation rules for a ship.

{
  "type": "automation_list",
  "ship_id": "660e8400-e29b-41d4-a716-446655440001"
}
Field Type Required Description
type string yes "automation_list"
ship_id uuid no Target ship (defaults to active ship; must be owned)

Validation errors: E034 if ship not owned.

On success: Server responds with automation_rules.


automation_copy

Copy automation rules from one ship to another. Both ships must be owned by the player. Copied rules are created disabled with unique names (conflict resolution appends " (N)" suffix).

{
  "type": "automation_copy",
  "source_ship_id": "550e8400-e29b-41d4-a716-446655440000",
  "target_ship_id": "660e8400-e29b-41d4-a716-446655440001",
  "rule_ids": ["rule-uuid-1", "rule-uuid-2"]
}
Field Type Required Description
type string yes "automation_copy"
source_ship_id uuid yes Ship to copy rules from
target_ship_id uuid yes Ship to copy rules to (must differ from source)
rule_ids array no Specific rule IDs to copy; if omitted, copies all rules

Validation errors: E026 if source/target missing or identical, E034 if either ship not owned, E026 if no valid rules found.

On success: Server responds with automation_copied.


ship_log_request

Request event log entries for a ship. Supports cursor-based pagination.

{
  "type": "ship_log_request",
  "ship_id": "550e8400-e29b-41d4-a716-446655440000",
  "limit": 50,
  "before_id": 100
}
Field Type Required Description
type string yes "ship_log_request"
ship_id uuid no Ship to query (defaults to active ship)
limit integer no Max entries to return (default: 50)
before_id integer no Return entries with ID less than this value (pagination cursor)

On success: Server responds with ship_log_history.


request_system_snapshot

Request a full snapshot of all entities in a star system. Used when switching views to a different system (e.g., galaxy view → system view).

{
  "type": "request_system_snapshot",
  "system_id": "alpha_centauri"
}
Field Type Required Description
type string yes "request_system_snapshot"
system_id string yes System to fetch entities for

Validation errors: E040 if system_id is missing.

On success: Server responds with system_snapshot.


switch_system

Request to move the player’s ship to a different star system. Not yet implemented — returns NOT_IMPLEMENTED error (see #736).

{
  "type": "switch_system",
  "system_id": "alpha_centauri"
}
Field Type Required Description
type string yes "switch_system"
system_id string yes Target system to travel to

Status: Placeholder — inter-system travel will be implemented via jump gates.


maneuver_query

Query the current maneuver state for the player’s active ship.

{
  "type": "maneuver_query"
}
Field Type Required Description
type string yes "maneuver_query"

On success: Server responds with maneuver_status (with active: true and maneuver details, or active: false and maneuver: null).


maneuver_abort

Abort the active maneuver on the player’s active ship.

{
  "type": "maneuver_abort"
}
Field Type Required Description
type string yes "maneuver_abort"

Sets the abort flag on the maneuver hash in Redis. The tick-engine reads this flag and stops the maneuver on the next tick.

On success: Server responds with maneuver_aborted. Errors: E028 if no active maneuver.


maneuver_pause

Pause the active maneuver on the player’s active ship.

{
  "type": "maneuver_pause"
}
Field Type Required Description
type string yes "maneuver_pause"

On success: Server responds with maneuver_paused. Errors: E028 if no active maneuver, E029 if already paused.


maneuver_resume

Resume a paused maneuver on the player’s active ship.

{
  "type": "maneuver_resume"
}
Field Type Required Description
type string yes "maneuver_resume"

On success: Server responds with maneuver_resumed. Errors: E028 if no active maneuver, E030 if not paused.


switch_ship

Switch the player’s active ship to a different ship in their fleet.

{
  "type": "switch_ship",
  "ship_id": "660e8400-e29b-41d4-a716-446655440002"
}
Field Type Required Description
type string yes "switch_ship"
ship_id uuid yes Ship to switch to (must be owned by the player)

On success: Server responds with ship_switched. Subsequent state broadcasts use the new ship as the player’s own ship. Errors: E033 if ship_id is empty.


fleet_list

Request a list of all ships owned by the player, with live state from Redis.

{
  "type": "fleet_list"
}
Field Type Required Description
type string yes "fleet_list"

On success: Server responds with fleet_list (server → client).


fleet_spawn

Spawn a new ship for the player’s fleet.

{
  "type": "fleet_spawn",
  "name": "Scout",
  "ship_class": "fast_frigate",
  "station_id": "990e8400-e29b-41d4-a716-446655440004",
  "system_id": "sol"
}
Field Type Required Description
type string yes "fleet_spawn"
name string no Ship display name (defaults to empty)
ship_class string no Ship class (default "fast_frigate")
station_id uuid no Station to spawn at (defaults to empty)
system_id string no Star system to spawn in (default "sol")

On success: Server responds with fleet_spawned. Errors: E031 if ship limit reached, E032 if invalid ship name.


fleet_abandon

Abandon a ship from the player’s fleet, removing it from the game.

{
  "type": "fleet_abandon",
  "ship_id": "660e8400-e29b-41d4-a716-446655440002"
}
Field Type Required Description
type string yes "fleet_abandon"
ship_id uuid yes Ship to abandon (must be owned by the player)

If the abandoned ship was the active ship, the server automatically switches to another ship in the fleet. Cannot abandon the last ship.

On success: Server responds with fleet_abandoned. Errors: E033 if ship_id is empty, E034 if ship not owned or last ship.


fleet_command

Issue a command to all fleet ships (excluding the active ship).

{
  "type": "fleet_command",
  "command": "follow_me"
}
Field Type Required Description
type string yes "fleet_command"
command string yes "follow_me" or "hold_position"

Commands:

  • follow_me — Deletes existing automation rules on each fleet ship and creates a station_keep rule targeting the active ship
  • hold_position — Deletes all automation rules on each fleet ship and sets thrust to 0

On success: Server responds with fleet_command_result. Errors: E035 if command is not recognized.


request_systems

Request the list of all star systems in the galaxy.

{
  "type": "request_systems"
}
Field Type Required Description
type string yes "request_systems"

On success: Server responds with galaxy:systems.


Data Types

Type Format Example
vec3 {"x": number, "y": number, "z": number} {"x": 1000.0, "y": 2000.0, "z": 3000.0}
quaternion {"w": number, "x": number, "y": number, "z": number} {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0}
uuid string "550e8400-e29b-41d4-a716-446655440000"
timestamp ISO 8601 string or Unix milliseconds integer "2025-01-15T10:30:00Z" or 1705315800000

Units

All values use SI units unless otherwise noted.

Property Unit Notes
position meters vec3, ICRF coordinates
velocity m/s vec3, ICRF coordinates
angular_velocity rad/s vec3, body frame
mass kg  
fuel kg  
fuel_capacity kg  
radius meters  
rotation_period seconds Negative = retrograde rotation
axial_tilt degrees  
prime_meridian_at_epoch degrees  
inertia_tensor kg·m² Diagonal elements only
thrust_level dimensionless 0.0 to 1.0
rotation input dimensionless -1.0 to 1.0 per axis
wheel_saturation dimensionless 0.0 to 1.0 per axis (0 = empty, 1 = saturated)

Derived and Computed Fields

Some fields in state messages are computed rather than stored directly:

Field Computed By Source
fuel_capacity physics Ship spec constant
mass physics dry_mass + current fuel
wheel_saturation api-gateway abs(wheel_momentum[axis]) / 10000

Rate Limiting

Message Type Limit Rationale
control 60/sec Continuous control inputs (60 Hz exceeds typical human input rates)
service 1/min Prevent abuse of fuel/reset
chat_send 5/sec Prevent chat spam

Per-tick coalescing means the effective update rate is bounded by tick rate regardless of input frequency.

Error Codes

Code Message Description
E001 Invalid rotation value Rotation not a dict, or axis value non-numeric, non-finite, or outside -1 to 1
E002 Invalid thrust value Thrust value non-numeric, non-finite, or outside 0 to 1
E003 Invalid service type Unknown service requested
E004 Rate limit exceeded Too many requests
E005 Authentication failed Invalid or expired token
E006 Ship not found Player’s ship does not exist
E007 Service unavailable Requested service temporarily unavailable
E008 Server error Internal server error
E009 Player not found Player account does not exist
E010 Username taken Username already registered
E011 Invalid username Username format invalid (3-20 chars, alphanumeric + underscore)
E012 Invalid password Password too short (minimum 8 characters)
E013 Server full Maximum concurrent connections reached (16)
E014 Account deleted Account deleted by administrator
E015 Invalid tick rate Tick rate outside valid range (0.1-100.0 Hz)
E016 Snapshot not found Requested snapshot does not exist
E017 Not initialized Physics service not yet initialized (startup race)
E018 Chat rate limited Too many chat messages (max 5/sec)
E019 Chat message too long Message exceeds 256 characters
E020 Chat message empty Message is empty after trimming
E022 Target not found Target entity does not exist
E023 Relative velocity too low Relative velocity too low for target prograde/retrograde
E024 Too close to target Too close to target for target/anti-target mode
E025 Rule limit reached Maximum 10 automation rules per ship
E026 Invalid automation rule Automation rule validation failed (invalid field, operator, action, etc.)
E027 Rule not found Automation rule does not exist or belongs to different ship
E028 No active maneuver No maneuver is currently running for this ship
E029 Invalid time scale Time scale outside valid range (0.1-100.0)
E030 Invalid ship_id Ship ID is empty or malformed
E031 Ship is not docked / Ship limit reached Docking: undock attempt when not docked. Fleet: player exceeded maximum ships per player
E032 Too far from station / Invalid ship name Docking: ship > 500 m from station. Fleet: ship name not 3-32 characters, or duplicate name for same player
E033 Relative velocity too high / Ship not found Docking: relative velocity > 10 m/s. Fleet: ship does not exist or not owned by player
E034 Station not found / Ship not owned Docking: station entity missing. Fleet: ship not owned by player, or cannot abandon last ship
E035 Unknown fleet command Fleet command not recognized (valid: follow_me, hold_position)

gRPC Failure Error Responses

When gRPC calls to the physics service fail while processing WebSocket commands (control, service, attitude_hold, and all attitude_* mode commands), the server sends an error message to the client with the following mapping:

gRPC Status Error Code Message
NOT_FOUND E006 Ship not found
UNAVAILABLE E007 Service unavailable
DEADLINE_EXCEEDED E007 Service unavailable
Any other failure E008 Server error

Additionally, the service command sends E003 if the service field is not "fuel" or "reset".

Message Size Enforcement

The server enforces a maximum WebSocket message size of 64 KB (65536 bytes). Messages exceeding this limit are rejected before JSON parsing to prevent memory exhaustion attacks.

Implementation: In the WebSocket message loop, read raw bytes first using receive_bytes(), check len(raw) > 65536, and close the connection with code 1009 (Message Too Big) if exceeded. Only parse JSON after the size check passes. This prevents an attacker from sending arbitrarily large payloads to exhaust server memory.

Invalid Message Handling

Condition Response Disconnect?
Malformed JSON Send error E008 No
Unknown message type Ignore silently No
Valid type, invalid payload Send error (E001-E003) No
Message too large (> 64 KB) Close connection (code 1009) Yes
Too many errors (> 10 in 60s) Send E004, close connection Yes

Unknown message types are silently ignored for forward compatibility (new message types don’t break old clients).

Reconnection

When the WebSocket connection closes (server restart, network interruption, ping timeout), the client reconnects automatically using exponential backoff with jitter.

Parameter Value
Initial delay 1 second
Multiplier 2× per attempt
Maximum delay 30 seconds
Jitter ×[0.8, 1.2] of computed delay
Reset On successful connection (onopen)

Backoff sequence (before jitter): 1s, 2s, 4s, 8s, 16s, 30s, 30s, …

Jitter prevents thundering herd when the server restarts and multiple clients reconnect simultaneously.

Client-Side Message Queue

When the WebSocket is not in the OPEN state (during reconnection), outgoing messages are queued in-memory and flushed in order after re-authentication succeeds on reconnect.

Parameter Value
Max queue size 100 messages
Overflow behavior Oldest message dropped (FIFO shift)
Excluded types ping (stale by definition)
Flush trigger After auth message sent in onopen handler

At 60 Hz control input rate, the 100-message cap covers approximately 1.6 seconds of disconnect. Longer disconnects naturally shed stale control data (sent most frequently) while preserving more recent attitude and service commands (sent less frequently, remain near the tail of the queue). Server-side per-tick coalescing handles any duplicate control messages that arrive in the flush burst.

Server-Side Reconnection Safety

When a player reconnects, the new WebSocket may be accepted before the old connection’s cleanup (finally block) runs. The server guards against this race condition: remove_connection(player_id, websocket) only removes the connection entry if the stored WebSocket matches the one being cleaned up. This prevents a stale cleanup from removing the player’s new active connection.

Disconnection Cleanup

When a player disconnects (WebSocket close), the server must clean up connection-scoped state only. Persistent game state (maneuvers, automation rules) must NOT be cleaned up on disconnect — they are long-running operations that survive reconnects.

State Key Pattern Cleanup Action
Online status connections:online SREM player_id
Targeting state In-memory map Remove entry, notify affected ships

NOT cleaned up on disconnect (persistent game state):

  • maneuver:{ship_id} — managed by tick-engine, cleaned up on completion/cancellation/ship deletion
  • automation:{ship_id}:* — managed by player, cleaned up on explicit deletion or ship deletion

Disconnection Behavior

  • Game world continues regardless of player connection status
  • Ship maintains current trajectory and thrust settings while disconnected
  • On reconnect, client receives current state and resumes control

Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.