Skip to content

Ship — Schema

Status: 🚧 Partial — Ship core (cargo/combat/quantum/NPC hulls) and ShipSpecification are committed; the entire ship-registry feature set (registered_owner_id, current_pilot_id, stolen_status, hatch_pin … · ⚠︎ contains code↔spec divergence (impl audit 2026-06-16)

A player-owned vessel; tracks position, cargo, combat readiness, and special equipment. Companion docs: ./ships.md's gameplay spec lives at ../FEATURES/gameplay/ships.md, and the registry workflow lives at ../SYSTEMS/ship-registry.md.

Schema status

Per ADR-0066 D-V1, schema-level implementation status is consolidated here rather than scattered across field descriptions. The descriptions below describe the target schema (per the doc philosophy: DATA_MODELS describes the target).

Design-only columns — present in the spec, not yet committed to migrations / code at the time of writing:

  • Ship: registration_number, registered_owner_id, current_pilot_id, stolen_status, stolen_reported_at, hatch_pin_code, salvage_break_in_progress_by_id, salvage_break_started_at, is_abandoned, abandoned_at, for_sale_price, for_sale_listed_by_id, destruction_cause, hangar, tow_state.
  • ShipSpecification: insurable flag.

All other columns on this page are committed (per the gameserver schema as of 2026-Q2).


Ship

Source: services/gameserver/src/models/ship.py

Purpose: A player-owned vessel; tracks position, cargo, combat readiness, and special equipment.

Fields:

name type constraints notes
id UUID PK
name String(100) not null
type Enum ship_type not null ESCAPE_POD, LIGHT_FREIGHTER, CARGO_HAULER, FAST_COURIER, SCOUT, COLONY, DEFENDER, CARRIER, WARP_JUMPER, NPC_MARSHAL_INTERDICTOR (target), NPC_SENTINEL_INTERDICTOR (target). The two NPC_* variants are filtered from any player-facing API that lists ship types and rejected from any registry-event that would transfer the hull to a player (ERR_NPC_ONLY_HULL); ShipSpecification.is_npc_only = true for both. See ../FEATURES/gameplay/police-forces.md.
registration_number String(15) unique, not null Player-visible stable identifier of the form REG-XXXX-YYYY (4-char alphanumeric block + 4-digit registration year). Immutable for the lifetime of the hull, even across ownership transfers. See ../SYSTEMS/ship-registry.md.
registered_owner_id UUID FK players.id not null, CASCADE Legal owner of record (formerly owner_id). Updates only via successful registry transfer.
current_pilot_id UUID FK players.id nullable, SET NULL Whoever's currently flying the ship; can be NULL (Drifting), the registered owner (Owner aboard), or any other player (Borrowed).
stolen_status Boolean default false True after the registered owner files a stolen report. Whoever's piloting becomes Wanted (see ../FEATURES/gameplay/ranking.md#wanted-status).
stolen_reported_at DateTime nullable Timestamp of the stolen report; cleared on retraction or successful transfer.
hatch_pin_code String(8) nullable 4–8 alphanumeric pin gating the boarding hatch. Random at registration. Cleared by salvage break (NULL = unlocked). Owner or current pilot can change it; owner can request a port-mediated reset (1h delay). See ../SYSTEMS/ship-registry.md#hatch-pin-lock.
salvage_break_in_progress_by_id UUID FK players.id nullable When non-NULL, a salvage break is underway on this Drifting ship by the named player. The break aborts on combat involving them or on them leaving the sector.
salvage_break_started_at DateTime nullable Start timestamp for the salvage break; duration scales by ship class (Scout 1h / Cargo Hauler 4h / Carrier 12h). On completion, hatch_pin_code is cleared.
is_abandoned Boolean default false True after the registered owner explicitly abandons the ship at a port. Free-claim by first taker; auto-archives after 7 days unclaimed.
abandoned_at DateTime nullable Timestamp of abandonment; auto-archive triggers at abandoned_at + 7 days.
for_sale_price Integer nullable When non-NULL, the ship is listed for peer-to-peer sale at the host port. Buyer at the same port can pay this price for an instant registry transfer. Seller can de-list (clear) at any time.
for_sale_listed_by_id UUID FK players.id nullable Player who listed the sale (always the registered owner at listing time).
sector_id Integer not null matches Sector.sector_id (integer), not the UUID
base_speed, current_speed Float not null
turn_cost Integer not null
warp_capable Boolean default false
is_active Boolean default true
status Enum ship_status default DOCKED DOCKED/IN_SPACE/IN_COMBAT/DESTROYED/MAINTENANCE/MINING/HARMONIZING (MINING reserved for asteroid harvesting and nebula Shard harvesting per ../FEATURES/economy/mining.md and ../FEATURES/galaxy/quantum-resources.md; HARMONIZING is the Warp Jumper's invulnerable-but-intact state during the 1-hour Phase 3 harmonization window — the hull stays alive and the destruction commit fires at timer completion, not at call time, per ../FEATURES/galaxy/warp-gates.md)
destruction_cause Enum destruction_cause nullable Set when status = DESTROYED. Values: COMBAT / HAZARD / SELF_DESTRUCT / ABANDONMENT_EXPIRED / WARP_GATE_ANCHOR (the Warp Jumper-consumed-at-Phase-3 case per ADR-0029). Drives Cargo-Wreck and insurance-payout branching at the destruction handler.
maintenance JSONB not null
cargo JSONB not null commodity → quantity
has_cloaking Boolean default false
genesis_devices, max_genesis_devices Integer defaults 0
mines, max_mines Integer defaults 0
has_automated_maintenance Boolean default false
combat JSONB not null shields/hull/etc.
attack_turn_cost Integer nullable per-ship combat initiation cost (added in migration d2e3f4a5b6c7)
upgrades JSONB default []
equipment_slots JSONB default {} added in migration d2e3f4a5b6c7. Holds slot-keyed equipment installs including quantum_harvester, tractor_beam, slipdrive, and sensor (the upgrade ladder L0–L3 referenced by Quantum Jump scan accuracy — see ADR-0030). The Warp Jumper's Quantum Charge consumable lives here under a special_equipment slot key per ADR-0030 (the per-jump Phase 2 commit cost; one charge consumed per Quantum Jump regardless of band). The sensor upgrade affects the Phase 1 long-range scan's misread rate: L0 = 15% misread, L1 = 10%, L2 = 5%, L3 = 0% (per ADR-0030 §Disclosure model).
hangar JSONB nullable only populated on capital-size ships (Carrier). Carrier ship-hangar state — see Carrier ship-hangar below. NULL on every other ship.
tow_state JSONB nullable only populated when this ship is actively towing another via Tractor Beam. See Ship tow state below. NULL otherwise. Mutually exclusive with being a docked passenger inside a Carrier hangar.
insurance JSONB nullable
is_destroyed, is_flagship Boolean defaults false
purchase_value, current_value Integer not null

Carrier ship-hangar

Ship.hangar JSONB shape on capital-size ships (only Carrier qualifies at launch). The hangar holds whole player ships in transit, separate from the Carrier's drone bay. Capacity is 8 size-units; per-ship size cost is 1 / 2 / 4 / 8 for tiny / small / medium / large.

{
  "capacity_units": 8,
  "docked": [
    {
      "ship_id": "uuid",
      "owner_id": "uuid",
      "size": "tiny | small | medium | large",
      "size_units": 1,
      "docked_at": "iso8601"
    }
  ]
}

docked is an append-only array per dock event; entries are removed on undock or jettisoned-on-Carrier-destruction. Sum of docked[*].size_unitscapacity_units. Validated server-side before each dock request resolves; the consent flow on the docking pilot side is an application-layer guard, not a schema constraint.

See ../FEATURES/gameplay/ships.md#carrier-hangar for the gameplay spec (dock/undock turn costs, combat behavior, jettison-on-destruction).

Ship tow state

Ship.tow_state JSONB on the hauler (the ship doing the towing) when a Tractor Beam tow operation is active. NULL otherwise.

{
  "towed_ship_id": "uuid",
  "towed_owner_id": "uuid",
  "towed_size": "tiny | small | medium | large",
  "surcharge_per_move": 1,
  "locked_at": "iso8601",
  "lock_sector_id": "integer"
}

surcharge_per_move is computed at lock-on from towed_size (1 / 2 / 4 / 8 → +1 / +2 / +3 / +5 turns) and cached on the row so the movement service doesn't re-traverse ShipSpecification on every move. lock_sector_id records where the lock-on happened, for audit and tow-distance metrics.

A ship with tow_state != NULL cannot itself be towed (no nesting), cannot dock into a Carrier hangar, and cannot fire its Tractor Beam in weapon mode (mutual exclusion). The towed ship's row is unmodified — its current_pilot_id stays set, but movement_service rejects independent move attempts while it's towed.

See ../FEATURES/gameplay/ships.md#tractor-beam-tow-operations for the gameplay spec (consent flow, travel restrictions, combat behavior, detach paths).

Relationships: - registered_ownerPlayer (FK registered_owner_id); the legal owner of record. - current_pilotPlayer (FK current_pilot_id, nullable); whoever's flying right now. - flagship_of back-ref via Player.current_ship_id (post-update cycle). - sectorSector joined on integer sector_id. - genesis_device_objectsGenesisDevice (1:many). - fleet_membershipFleetMember (1:1). - registry_historyShipRegistry (1:many) — append-only audit trail of ownership changes.


ShipRegistry

Append-mostly history of every ship's registration record. Persists even after the ship is destroyed.

Source: services/gameserver/src/models/ship_registry.py (target — does not yet exist).

Purpose: Public ship-ownership ledger. Every ship has at least one row (its initial registration) and gains a row each time ownership changes or the stolen-status flips.

Fields:

name type constraints notes
id UUID PK
ship_id UUID FK ships.id not null the live ship (or DESTROYED ship row, retained for history)
registration_number String(15) not null, indexed matches Ship.registration_number; redundant for fast lookup
event_type Enum registry_event not null INITIAL_REGISTRATION, OWNERSHIP_TRANSFER, STOLEN_REPORTED, STOLEN_RETRACTED, IMPOUNDED, ARCHIVED
original_owner_id UUID FK players.id not null first-ever registered owner; immutable across all rows for a given ship
previous_owner_id UUID FK players.id nullable for transfer events; null for initial registration
new_owner_id UUID FK players.id nullable for transfer events; null for stolen-report / archive events
acting_party_id UUID FK players.id nullable who triggered the event (e.g., the claimant in a transfer; the owner who reported stolen)
transfer_fee_paid Integer nullable credits paid in a transfer event
port_id UUID FK stations.id nullable port where the event was registered
created_at DateTime not null event timestamp
metadata JSONB default {} event-specific extra data (dispute outcome, impound reason, etc.)

Relationships: - shipShip (FK). - original_owner, previous_owner, new_owner, acting_partyPlayer (FKs). - portStation (FK).

Lifecycle: rows are inserted, never updated or deleted. The full history of a ship's ownership is queryable by ship_id ordered by created_at. After the ship is destroyed, an ARCHIVED row closes out the registry; Ship.status = DESTROYED but the registry row remains for investigations / lookup.

Indexes: (ship_id, created_at) for chronological lookups; (registration_number) for player-facing registration lookup; (original_owner_id) for "ships I've ever owned" queries.


ShipSpecification

Source: services/gameserver/src/models/ship.py (same file)

Purpose: Static balance data per ShipType — base cost, hull, shield, cargo, drone, evasion, attack/defense ratings, etc. One row per ship type.

Key fields: type (unique), base_cost, speed, turn_cost, max_cargo, max_colonists, max_drones, max_shields, shield_recharge_rate, hull_points, evasion, genesis_compatible, max_genesis_devices, warp_compatible, warp_creation_capable, quantum_jump_capable, scanner_range, attack_rating, defense_rating, attack_turn_cost, maintenance_rate, construction_time, fuel_efficiency, max_upgrade_levels (JSONB), special_abilities (JSONB), acquisition_methods (JSONB), faction_requirements (JSONB), insurable (Boolean default true — false on Warp Jumper and Escape Pod per ADR-0029; insurance UI hides tier selector when false).

size enum: tiny / small / medium / large / capital. Set per ShipType from the canonical roster (see ../FEATURES/gameplay/ships.md#ship-size-axis). Drives Carrier hangar fit checks and Tractor Beam tow surcharge. Belongs on the spec, not the per-hull Ship row, because every hull of a given type shares the same size.

scanner_range is the integer adjacent-sector visibility radius (Scout 5, Warp Jumper 8 highest, etc. — see ../FEATURES/gameplay/ship-roster.md). Independent from the Warp Jumper's long-range quantum scan (which projects a fuzzy resonance reading 5–10 sectors out along a chosen bearing — see ADR-0031). The two abilities are distinct: scanner_range governs adjacent-sector content visibility on every ship; quantum scan is a Warp-Jumper-only multi-sector probe.