Skip to content

Ship Registry

Status: 📐 Design-only — Nothing of the Ship Registry exists: no ownership/possession columns on Ship, no ShipRegistry model, no registry service … (impl audit 2026-06-16)

Purpose

The Ship Registry is the universal, persistent record of who legally owns each ship in the universe. It separates possession (who's flying the ship right now) from ownership (whose name is on the registration). This separation enables a rich social/legal layer:

  • Borrowing — anyone can pilot an abandoned ship without consequence; you don't need permission to climb into a parked vehicle.
  • Theft — the registered owner can flip a ship's status to STOLEN, at which point anyone piloting it is in serious legal trouble.
  • Salvage — paying a registration-transfer fee at a port legally moves the ship from one owner to another; replaces the old "claim an abandoned ship" model.
  • Insurance integrity — payouts always go to the registered owner, regardless of who was flying when the ship went down.

The registry is append-mostly — registrations are never deleted, even after the ship is destroyed. Historical records support investigations, faction reputation calculations, and cross-region travel verification.

Inputs

The registry reads (canonical column names per ../DATA_MODELS/ships.md): - Ship.id (UUID FK to the live ship row). - Ship.registration_number (player-visible stable string, lives on Ship not ShipRegistry). - Ship.registered_owner_id (current legal owner; lives on Ship). - Ship.stolen_status boolean and Ship.stolen_reported_at timestamp (live state on Ship). - Ship.current_pilot_id (whoever's at the controls right now; can differ from registered owner). - ShipRegistry.original_owner_id (immutable; first registered owner — recorded once in the initial registry event row). - ShipRegistry event-log rows (event_type, previous_owner_id, new_owner_id, acting_party_id, transfer_fee_paid, port_id, created_at, metadata) form the append-only audit trail.

The registry fires on: - Ship purchase / construction — new registry entry created; original_owner_id and registered_owner_id both set to the buyer. - Manual ejectShip.current_pilot_id = NULL; ownership unchanged. Ship enters Drifting state. - BoardingShip.current_pilot_id = boarder.id; ownership unchanged. Ship enters Borrowed state if boarder ≠ registered owner. - Stolen report filedstolen_status = True, stolen_reported_at = now(). Ship now in Stolen state. - Stolen report retractedstolen_status = False, log retraction. - Registration transfer — multi-step contested transfer flow (see below); on success, registered_owner_id updated, stolen_status cleared. - Ship destructionShip.status = DESTROYED; registry retains the entry as historical record. CargoWreck spawns separately (see ../FEATURES/gameplay/ships.md#cargo-wreck). The Ship.destruction_cause enum (📐 Design-only) records why: COMBAT, HAZARD, SELF_DESTRUCT, ABANDONMENT_EXPIRED, and WARP_GATE_ANCHOR (the Warp Jumper hull consumed at Phase 3 of warp gate creation per ADR-0029 — does not spawn a CargoWreck since the dismantle is planned, not explosive; does not fire an insurance payout because Warp Jumpers are non-insurable per ../FEATURES/gameplay/ship-insurance.md#non-insurable-ships).

Process

Registration number format

Each ship gets a stable, player-visible registration_number of the form REG-XXXX-YYYY:

  • XXXX — 4-character alphanumeric block, randomly generated at registration time, uppercase A-Z + 0-9 (excluding ambiguous I/O/0/1).
  • YYYY — 4-digit registration year (game-time, not real-time; matches the in-universe calendar).

Examples: REG-A47B-2103, REG-XQF9-2104.

Registration numbers are immutable — they never change, even when ownership transfers. If you see REG-A47B-2103 flying around, it's the same hull that was first registered in year 2103, regardless of who's currently piloting or owns it.

Six ownership-affecting events

Possession (who's flying) and ownership (whose name is on the registration) move through six distinct events:

Event Pilot changes? Ownership changes? Triggered by
Drift Owner ejects → no pilot No Owner manual-eject from intact ship
Borrow Stranger boards → new pilot No Boarder enters with the pin (or after a salvage break)
Trade Yes (buyer takes possession) Yes (sale) Owner + buyer both at same port; price agreed; credits transfer
Abandon NULL → first taker Yes (relinquished, no fee) Owner explicit "abandon" action at a port
Salvage Claimant takes possession Yes (contested transfer) Claimant pays 30% transfer fee at port; 24h owner-dispute window
Steal Pilot != owner; owner reports stolen No (still legally owner's) Owner files stolen report on a Borrowed ship

These compose freely — e.g., a stranger can salvage-break a Drifting ship → enter (Borrow) → take the ship to a port and Salvage-claim it; or the owner could come back and Borrow it back (since they have the pin), or report-Steal from a port if they prefer.

Ship state machine

        [purchase or construction]
                 │
                 ▼
       Owner aboard ──── manual eject ────► Drifting (locked) ◄────────┐
            ▲    ▲                              │                       │
            │    │                              │ pin entered           │
            │    │                              │ OR salvage break      │
            │    │                              ▼                       │
            │    │                          Borrowed ◄──────────────────┤
            │    └────── owner re-boards ──┤   │                        │
            │                              │   │ owner reports stolen   │
            │                              │   ▼                        │
            │                              │  STOLEN ────────────►──────┤
            │                              │   │                        │
            │ trade at port (sale)         │   │ retract report         │
            ├──────────────────────────────┘   │                        │
            │                                  ▼                        │
            │ salvage-claim at port (24h)  Borrowed/Drifting/Owner ─────┘
            ├──────────────────────────────────│
            │                                  │ destroyed in combat
            │ abandoned at port (giveaway)     ▼
            ├────────────────────────► (registry archived,
            │                            CargoWreck spawns)
            │
            ▼
       Owner aboard (new owner)

State definitions

  • Owner aboardcurrent_pilot_id == registered_owner_id; the registered owner is flying it.
  • Driftingcurrent_pilot_id IS NULL; nobody's flying. The ship sits in its sector, visible to all players. The hatch is locked by the owner's pin; entry requires either knowing the pin or completing a salvage break.
  • Borrowedcurrent_pilot_id != NULL and current_pilot_id != registered_owner_id; someone other than the legal owner is flying. Not a crime by default. The borrower can change the pin (locking the original owner out, but exposing themselves to a Stolen report).
  • Abandonedis_abandoned = True, current_pilot_id IS NULL, registered_owner_id retained for history. Free to claim by anyone at a port; first claimant becomes the new registered owner without paying a transfer fee. Auto-archives if unclaimed for 1 week.
  • For Salefor_sale_price IS NOT NULL AND for_sale_listed_by_id set; ship is listed at a port for peer-to-peer purchase. Owner can de-list at any time. Buyer at the same port pays the price; transfer is instant.
  • Stolenstolen_status = True; the registered owner has filed a stolen report. Whoever's piloting is now a thief and enters Wanted Status.

Eject and board (the canonical piloting flow)

A player can own any number of ships but can only pilot one at a time — the one whose current_pilot_id matches the player's id, also pointed to by Player.current_ship_id. Changing which ship you're flying is always two underlying operations:

POST /api/v1/players/me/eject
Effects: current ship's current_pilot_id cleared; ship transitions to Drifting in its current sector; Player.current_ship_id cleared (player is briefly in escape-pod state).

POST /api/v1/ships/{ship_id}/board
{ "pin": "...optional, required if not registered owner..." }
Effects: validates the player can board (registered owner, OR pin matches, OR ship was previously salvage-broken); sets Ship.current_pilot_id = player.id; sets Player.current_ship_id = ship.id; ship transitions to Owner-aboard, Borrowed, or Stolen-piloted (the last if the ship has stolen_status = True).

Clients typically chain these as a single "switch ship" UX action — eject + board in one click. There is no separate "swap" endpoint; the registry doesn't need one.

Turn cost for the eject + board pair:

Where Turn cost Reason
Both ships docked at the same port 0 turns Safe, planned switching at a single location. Most common case.
Both ships in the same sector but in space 1 turn One-turn friction discourages loadout-cycling mid-fight without making routine in-space switches painful.
Ships in different sectors n/a Cannot eject + board across sectors; you must travel to the target ship first.

Eligibility for board: - Registered owner of the target → always eligible (no pin required). - Pin known by the boarder → eligible regardless of ownership (this is regular Borrow). - Post-salvage-break target (hatch pin cleared) → eligible to anyone in the sector. - Wanted-trigger — boarding a stolen_status = True ship while you are not the registered owner immediately enters Wanted Status (see above).

Failed boarding (no pin and ship still locked) returns 403 Forbidden with the boarding-rejected reason.

Hatch pin lock

Every ship has a pin code that gates boarding:

  • Ship.hatch_pin_code — string, 4–8 alphanumeric characters, owner-settable.
  • Default at registration: a randomly-generated pin is set at ship creation. The owner sees it in their ship panel from minute one. To let a friend or team-mate borrow, the owner shares the string out-of-band (chat, voice — the system doesn't track who knows it).
  • Boarding flow: when a non-owner attempts to board a Drifting ship, the client prompts for the pin. Wrong pin → boarding rejected. Correct pin → ship enters Borrowed state.
  • Pin changes: the current pilot — owner OR borrower — can change the pin while aboard. A borrower who changes the pin locks the owner out of their own ship; the owner's recourse is to file a stolen report.
  • Pin recovery: the registered owner can always reset the pin via a port admin action (1-hour real-time delay before the new pin takes effect; gives any current borrower a window to extract themselves before being locked out).
  • No pin tracking: the system does not maintain an allowlist of who knows the pin. If it leaks, the owner changes it.

Salvage break

A stranger without the pin can force entry to a Drifting ship via a salvage break — a slow, sector-bound operation that simulates physically cutting through the hatch.

Duration scales with ship class:

Ship class Salvage break time
Scout, Fast Courier, Light Freighter 1 hour real-time
Cargo Hauler, Defender, Colony Ship 4 hours
Carrier, Warp Jumper 12 hours
Escape Pod n/a — escape pods cannot be salvage-broken

Mechanics:

  1. Salvager dispatches a POST /api/v1/ships/{ship_id}/salvage-break request while in the same sector as the Drifting target.
  2. Server acquires a row-level lock on the target Ship row (SELECT ... FOR UPDATE NOWAIT per ADR-0049). Concurrent attempts on the same ship reject immediately with ERR_SALVAGE_BREAK_IN_PROGRESS carrying the in-progress salvager's id and ETA — no queue, no silent serialization.
  3. Server records Ship.salvage_break_in_progress_by_id = salvager.id and salvage_break_started_at = now().
  4. Salvager must remain in the sector until the timer completes. Leaving the sector (warp, dock, anything that changes Player.current_sector_id) cancels the operation and clears the row lock; the timer resets to zero and a new salvager can start a fresh break.
  5. Combat involving the salvager interrupts the operation; the timer resets to zero, lock clears.
  6. Salvager disconnect: a watchdog at salvage_break_started_at + salvage_break_duration auto-clears the lock at duration expiry. A separate periodic sweep handles stuck locks where the salvager went idle (defaults to 2× the duration before forced clear).
  7. The original owner returning to the sector does NOT auto-cancel — but the owner can attack the salvager (interrupting via combat) or simply re-board their ship using the pin (instantly aborting the break by occupying the ship).
  8. On timer completion, Ship.hatch_pin_code is cleared (set to NULL); the ship is now unlocked. The salvager — and anyone else in the sector — can board freely. The lock clears.
  9. After boarding, the new pilot can set their own pin (which locks subsequent borrowers out, including the original owner).

The break operation is visible in sector-presence: while in progress, all players in the sector see "salvage break on REG-XXXX-YYYY, ETA: X hours" — making it a contestable event. Other players can interfere with combat or by racing to complete their own break.

📐 Design-only — Ship.salvage_break_in_progress_by_id and salvage_break_started_at are launch-target columns; not in code today.

Reporting a ship stolen

The registered owner can file a stolen report at any time via:

POST /api/v1/ships/{ship_id}/report-stolen
Body: { "recovery_mode": "with_bounty" | "no_bounty" }

Recovery mode choice (per ADR-0052 SK37): the owner picks one of two recovery vehicles. The choice is preserved on the ship row and propagates to the destruction handler:

Mode Auto-bounty placed? Insurance fires on destruction? Trade-off
with_bounty (default for insurable = true hulls) Yes — 50% of ship value No Owner uses the bounty as their chosen resolution. Insurance is forfeit because the owner self-paid for the destruction. Closes the insurance × auto-bounty owner-collusion exploit.
no_bounty (default for insurable = false hulls) No Yes (when applicable) No bounty hunter incentive; the ship may stay stolen longer. Insurance fires normally if the ship is later destroyed by external hostiles — moot for non-insurable hulls (no payout exists), but recovery-preferred is the better default since destruction is total loss for those owners.

The rule: one recovery method per stolen-ship event, not both. Switching modes is permitted by retracting and re-filing (paying the posting fee each time).

Default by ShipSpecification.insurable (per ADR-0055 S-F4): when recovery_mode is omitted from the request body, the default is with_bounty for insurable hulls and no_bounty for non-insurable hulls. The owner can override either way; ARIA narrates the trade-off when the filer is on a non-insurable hull.

Effects: 1. Ship.stolen_status = True, stolen_reported_at = now(), Ship.stolen_recovery_mode = <chosen>. 2. The ship's registration number flags as STOLEN in any registry lookup. 3. The current pilot (if any) immediately enters Wanted Status (see ranking.md). 4. Realtime alert broadcast to nearby NPC patrols (📐 design-only — patrol AI hooks). 5. If recovery_mode = with_bounty: the owner is debited 50% of the ship's last appraised value from their wallet, and that amount is placed as a player-funded bounty on the bounty board (see bounties.md). The auto-stolen-report bounty rides the same player-paid escrow path as a manually placed bounty — it is not system-minted; the owner's credits are the fund. The standard 10% bounty placement fee is waived for auto-stolen-report bounties (the owner has already lost the use of their ship; the registry doesn't double-tax). If the owner has insufficient credits, the report is rejected up-front with ERR_INSUFFICIENT_CREDITS_FOR_AUTO_BOUNTY and the stolen flag is not set — the owner can either earn the credits and re-file, or file with recovery_mode = no_bounty (no bounty placed; insurance preserved on destruction per SK37). 6. Realtime UI: the ship appears in red on the owner's tracker; if it's in a sector the owner has presence in, an alert fires.

The owner can retract the stolen report at any time:

POST /api/v1/ships/{ship_id}/retract-stolen-report

Retracting clears the stolen_status flag and removes the auto-placed bounty. The bounty refund schedule depends on retract timing: retract within 24 hours of report = 75% refund (25% retention covers the bounty-board posting cost, false-report damping, and processing); retract after 24 hours = no refund (the bounty has been on the board long enough that other players may have made decisions based on its presence). No reputation penalty for false reports — the owner is presumed to know who they lent to — but the credits are non-trivial, which discourages frivolous filing.

Anti-collusion lock (per ADR-0049): the retract handler runs an atomic check inside the same transaction as the refund. If any BountyClaim row exists with target_player_id = ship.current_pilot_id (the thief), placed_at >= ship.stolen_reported_at, and collected_at IS NOT NULL, the retract rejects with ERR_BOUNTY_ALREADY_COLLECTED. This closes the file → kill → retract → refund collusion cycle at the most direct point — once a kill has fired the bounty, the report can no longer be retracted. The 24h grace remains for legitimate "false-alarm" cases where no kill happened.

Auto-bounty target (per ADR-0054 X-V2): when a stolen-report fires the auto-bounty, the bounty is placed on the thief's Player.id, not the stolen ship. Schema: BountyClaim.target_player_id (FK Player.id). The bounty stays attached to the thief regardless of what happens to the ship — if the thief destroys the ship to escape consequences, the bounty persists on their player and pays out when they're killed by any hunter. Multiple distinct owners filing stolen reports on the same thief stack bounties on the thief's player, creating cumulative pressure (intentional). Closes the trapped-escrow case where a destroyed ship row would otherwise strand the bounty escrow.

Bounty uniqueness constraint (per ADR-0055 S-V3): partial unique index BountyClaim UNIQUE (placer_player_id, target_player_id) WHERE collected_at IS NULL — one active bounty per (placer, target) pair. The same placer cannot stack multiple bounties on the same target; multiple distinct placers still stack. The "one active stolen-report per ship" half of the original concern is already covered by Ship.stolen_status being a single-valued boolean on the ship row.

Same-team collusion block (per ADR-0055 S-F1). Two rejection rules close the third-party-laundering loop:

  • Filing rejected with ERR_THIEF_IS_TEAM_MATE if ship.current_pilot_id (the alleged thief) shares a team with the placer at file-time. Filing against own team is not a real theft.
  • Bounty collection rejected with ERR_COLLECTOR_SAME_TEAM_AS_PLACER if collector_player_id shares a team with placer_player_id at collection-time. Cross-team bounty hunting still works; same-team laundering is blocked.

Both checks query live team membership (team_members join), not a snapshot — players changing teams to dodge the rule are blocked the moment the gated action fires. The collection-side check, when it rejects, leaves the BountyClaim row untouched (escrow still held); the placer can retract or wait for a non-team-mate kill.

Retract-grace expiry ticker (per ADR-0053 WR10). A periodic service runs every 60 seconds scanning Ship rows where stolen_status = True AND stolen_reported_at + 24h <= now() AND retract_grace_processed = false. For each: sets retract_grace_processed = true. After the flag is set, retract attempts no longer get the 75% refund — they fall through to the standard "no refund after 24 hours" rule. The flag is set-once and never cleared on the same report; a retract + refile produces a new stolen_reported_at and a fresh grace window with retract_grace_processed = false again. New schema: Ship.retract_grace_processed BOOLEAN DEFAULT false.

Wanted Status (pilot of a stolen ship)

When a player pilots a ship flagged stolen_status = True, they enter Wanted Status — a stronger flag than Suspect Status. While Wanted:

  • Player.wanted_status = True, Player.wanted_until = NULL (lasts as long as they're piloting the stolen ship).
  • Name color renders red in all UI surfaces (overrides personal reputation tier color).
  • Federation-zone immunity is suspended — anyone may attack a Wanted player in fed-space without normal policing penalties.
  • NPC patrols actively hunt — sector-presence broadcasts trigger NPC pursuit (📐 design-only).
  • Automatic bounty placed by the owner (see above) — typically 50% of ship value.
  • Personal reputation hit−100 / day while still in possession of the stolen ship; cumulative.
  • Port-docking impound — at any port, attempting to dock with a stolen ship results in:
  • Ship impounded; returned to registered owner (held at the port for the owner to collect).
  • Pilot fined: 25% of their credits (clamped to 100k cr max).
  • Pilot personal reputation hit: −100.
  • Pilot ejected to escape pod.
  • Wanted Status auto-clears the moment the pilot is no longer in the stolen ship (e.g., ejects, ship is destroyed, ship is impounded).

Wanted is mechanically distinct from Suspect Status (which fires from grace-window CargoWreck salvage). They can co-occur — but Wanted's effects dominate.

The new repo's "Salvage" acquisition method is registered as a contested transfer at a port:

POST /api/v1/ports/{port_id}/ship-registry/transfer
{ "ship_id": "...", "claimant_id": "..." }

Flow:

  1. Eligibility check — the ship must be Drifting (no current pilot) and not flagged stolen, OR Borrowed by the claimant for at least 1 hour. (Boarding doesn't grant transfer rights immediately; you can't board and instantly claim.)
  2. Fee assessment — claimant pays 30% of the ship's appraised market value (current value, accounting for damage / age / upgrades).
  3. Original-owner notification — registry pings the registered owner via realtime: "A registration transfer has been requested for REG-A47B-2103. You have 24 real-time hours to dispute."
  4. Dispute window — during the 24h window:
  5. Owner takes no action → transfer completes automatically at the deadline.
  6. Owner files stolen report → transfer is canceled, ship enters Stolen state, claimant is now Wanted (see above).
  7. Owner explicitly approves → transfer completes immediately.
  8. On completionregistered_owner_id updated to the claimant; the registry logs the transfer with timestamps, fee paid, and the original-owner-notification trail. Insurance from the previous owner voids.

The 24h window is what makes the system work: it gives the original owner a real chance to respond. If they're inactive, the ship transfers — protecting against indefinite squat. If they're active, the system surfaces the issue clearly.

Repair handling. Per legacy "may require repairs" — the ship may be transferred with damage. The new owner pays repair costs at standard rates after taking ownership. The transfer fee covers the registration; repair is separate.

Rare finds. 5% of contested-transfer ships have a small bonus on completion: residual cargo, an installed upgrade not the owner had on record, or a unique cosmetic modifier. 📐 Design-only — RNG table TBD.

Trading (peer-to-peer sale)

Two players can transact directly while both docked at the same port:

POST /api/v1/ports/{port_id}/ship-registry/sell
{ "ship_id": "...", "buyer_id": "...", "price": 250000 }

Flow:

  1. Mutual presence check — both players must be docked at the same port; the seller must be the registered owner; the ship must be in their inventory at the same port (i.e., not Drifting in space). Buyer must have credits ≥ price.
  2. Buyer confirmation — buyer receives a realtime confirmation request showing ship registration, price, and condition (hull, shields, upgrades, cargo). Buyer accepts or declines.
  3. On accept — credits transfer (buyer → seller); registry update is instant (no 24h dispute window because both consented); insurance from the previous owner voids.
  4. On decline / timeout (5 minute response window) — listing expires; no transfer, no fee.

Trading is the consensual cousin of contested salvage transfer: same registry-update mechanism, but mutual agreement bypasses the dispute window. Suitable for negotiated hand-offs to friends, team-mates, or buyers found through external coordination. The full trade protocol — including credits, cargo, and equipment alongside ships — is governed by ADR-0089.

No marketplace listings at launch. Ships cannot be listed publicly for general sale — both seller and buyer must be present at the port simultaneously. Discovery happens through external channels (chat, team coordination, etc.). If marketplace browsing becomes desirable later, it'd be a future ADR.

Abandonment

The registered owner can voluntarily relinquish ownership at a port:

POST /api/v1/ports/{port_id}/ship-registry/abandon
{ "ship_id": "..." }

Effects:

  1. Owner must be docked at the port and the ship must be at the same port (i.e., not in space and not currently Borrowed by someone else).
  2. Owner is ejected to escape pod; ship enters Abandoned state.
  3. Ship.is_abandoned = True, Ship.abandoned_at = now(), Ship.registered_owner_id retained for history but flagged as no-longer-active.
  4. The ship sits at the port (or in the sector if it was undocked); the first player to dock at the port and claim it becomes the new registered owner — no transfer fee, no dispute window, no contestable transfer. It's a true giveaway.
  5. 1-week auto-archive: if the ship is unclaimed for 7 real-time days, the registry auto-archives it; the ship row enters DESTROYED status with cause = ABANDONMENT_EXPIRED and a CargoWreck spawns containing whatever cargo was aboard.

Abandonment is irreversible — once the action is filed, the previous owner cannot un-abandon. They could re-claim the ship like anyone else if it's still unclaimed, but they don't get priority.

📐 Design-only — Ship.is_abandoned and Ship.abandoned_at are launch-target columns.

Insurance interaction

Insurance follows the registered owner, not the current pilot:

  • Drifting / Borrowed ship destroyed in combat — payout goes to registered_owner_id, not the borrower-pilot. The borrower gets nothing (and probably faces the registered owner's wrath socially).
  • Owner aboard ship destroyed — standard payout to owner.
  • Stolen ship destroyed — payout still goes to the registered owner. Theft does not void insurance.
  • Ownership transfer — insurance voids on transfer. New owner buys fresh coverage.

This makes insurance an asset-tracking policy, not a pilot-tracking policy. Aligns with how vehicle insurance works in real life.

Outputs / state changes

Per any registry action: - New ShipRegistry row inserted (immutable history) when ownership changes or stolen status flips. - Ship.registered_owner_id, Ship.current_pilot_id, Ship.stolen_status, Ship.stolen_reported_at mutated as needed. - Realtime events on the bus: - ship.registry_transfer_requested — to the registered owner; payload {ship_id, registration_number, claimant_id, dispute_deadline}. - ship.stolen_reported — to the current pilot (if any); becomes Wanted. - ship.stolen_retracted — to the current pilot; Wanted clears. - ship.registry_transferred — to the new owner and previous owner.

Invariants

  1. Every ship has exactly one registration_number, immutable for the lifetime of the hull.
  2. ShipRegistry.original_owner_id is set at creation and never changes.
  3. Ship.registered_owner_id is the current legal owner; updates only via registration transfer.
  4. Ship.current_pilot_id can be NULL (Drifting), registered_owner_id (Owner aboard), or any other player UUID (Borrowed). It can also be the pilot of a Stolen ship — independent of ownership.
  5. Ship.stolen_status = True requires a stolen_reported_at timestamp. Cleared on retraction or successful transfer.
  6. Insurance payouts always credit registered_owner_id, never the pilot.
  7. After ship destruction the Ship row enters DESTROYED, but ShipRegistry rows remain for historical lookup.

Failure modes

Mode Target handling
Original owner doesn't respond to transfer notification After 24h, transfer completes automatically.
Original owner files stolen report mid-transfer Transfer canceled; claimant becomes Wanted. Fee refunded.
Pilot of a Stolen ship enters fed-space Federation patrols pursue. Player-attackers don't suffer normal fed-space penalties.
Pilot of a Stolen ship docks at a port Ship impounded; pilot fined and ejected to escape pod.
Concurrent transfer requests on the same ship First request locks the ship until resolved (24h dispute or earlier outcome). Second request returns 409 Conflict.
Owner reports a ship they don't own Registry rejects the request (FK enforcement on registered_owner_id).
Insurance + Stolen + destroyed Owner collects payout; pilot's Wanted status clears (ship gone). Owner can also collect any auto-placed bounty if a third party kills the pilot.
Player tries to board their own destroyed ship Reject; the ship row is DESTROYED, not Drifting. CargoWreck mechanics apply instead.

Source map

Concern Path (target)
ShipRegistry model services/gameserver/src/models/ship_registry.py (target)
Ship state additions services/gameserver/src/models/ship.py (registered_owner_id, current_pilot_id, stolen_status, stolen_reported_at)
Registry service services/gameserver/src/services/ship_registry_service.py (target — handles report, retract, transfer flow)
Wanted status logic services/gameserver/src/services/wanted_service.py (target)
Combat hook (Wanted suspends fed-space immunity) services/gameserver/src/services/combat_service.py (extension)
Port impound logic services/gameserver/src/services/port_service.py (extension)
Auto-bounty on stolen-report services/gameserver/src/services/bounty_service.py:place_auto_bounty (extension)
Realtime registry events services/gameserver/src/services/websocket_service.py (event taxonomy in ./realtime-bus.md)