JSONB schemas¶
Status: 🚧 Partial — Galaxy, Cluster, WarpTunnel, and SpecialFormation JSONB shapes fully match code.
Ship.cargois now documented as the live shipped shape (WO-CANON-DRAFT-SHIP-CARGO-JSONB-SHAPE-DRIFT, 2026-08-05):{capacity, used, contents}— never the abandonedmax_capacity/used_capacityrename, and not a flat commodity→quantity map. Other Ship JSONB shapes on this page (maintenance, combat, upgrades, equipment_slots) remain launch-target sketches pending their own re-audit.
Most domain models lean on PostgreSQL JSONB columns rather than fully-relational sub-tables. This page documents the authoritative shape of each JSONB column so consumers can write to it without guessing.
Ship JSONB except cargo — still a migration sketch.
Ship.maintenance,Ship.combat,Ship.upgrades,Ship.equipment_slots, and theShipSpecificationshapes forspecial_abilities,acquisition_methods,faction_requirementsmay still diverge from live writers; treat those sections as launch targets until re-audited.Ship.cargobelow is live truth, not a target.
Conventions used below:
- Source. The model file that owns the column.
- Shape. Canonical JSON skeleton. Where a key's value is a primitive, the type is annotated as
int,str,float,bool, oriso8601. Where it's a collection, the inner element shape follows. - Defaults. The literal default a freshly-inserted row receives.
{}means "empty object";[]means "empty array";nullmeans SQL NULL. - Constraints. Application-level validation. The database does not enforce these; the gameserver does.
The contracts in this page apply to both JSONB (the explicit Postgres type) and the handful of columns declared as plain JSON (e.g. Fleet.battle_log).
Player¶
Source: services/gameserver/src/models/player.py
Player.reputation¶
{
"<faction_code>": {
"score": int,
"tier": str,
"last_updated": iso8601
}
}
Defaults: {} (empty; populated lazily on first interaction with a faction).
Constraints:
- Keys must match Faction.code.
- score clamped to [-1000, 1000].
- tier is the cached tier name derived from score; the source of truth is Reputation rows in reputations. The JSONB cache exists for fast reads on the player record.
Player.settings¶
{
"ui": {
"theme": str,
"compact_layout": bool,
"minimap_position": str
},
"language": {
"code": str,
"manual_override": bool
},
"audio": {
"master_volume": float,
"music_volume": float,
"sfx_volume": float,
"voice_volume": float,
"muted": bool
},
"accessibility": {
"colorblind_mode": str,
"reduced_motion": bool,
"screen_reader_hints": bool,
"subtitle_size": str
},
"gameplay": {
"auto_refuel": bool,
"confirm_destructive_actions": bool,
"default_trade_quantity": int,
"show_aria_suggestions": bool
},
"privacy": {
"show_on_leaderboards": bool,
"show_personal_reputation_publicly": bool,
"allow_team_invites": bool,
"allow_dms": str
},
"notification_preferences": {
"combat": bool,
"trade": bool,
"social": bool,
"system": bool,
"medals": bool
},
"medal_privacy": {
"visibility": str,
"pinned_medal_id": "str | null",
"curated_medal_ids": [str],
"show_hidden": bool,
"show_count_publicly": bool,
"broadcast_to_team": bool,
"unviewed_awards": [str]
},
"bounties": [
{
"id": str,
"placed_by": str,
"placed_by_name": str,
"amount": int,
"placed_at": iso8601,
"type": str
}
]
}
Defaults: {} — keys are populated lazily as the player interacts with the relevant feature. The gameserver supplies sane defaults at read time when a key is missing (e.g. ui.theme = "dark", language.code = inherit-from-region, audio.master_volume = 0.8, notification_preferences.medals = true, medal_privacy.visibility = "all", medal_privacy.show_hidden = false, medal_privacy.broadcast_to_team = true, medal_privacy.show_count_publicly = true).
Constraints:
- bounties is appended to by the bounty service (services/gameserver/src/services/bounty_service.py); each entry is one active bounty placed against this player.
- notification_preferences must contain only known channel keys (combat, trade, social, system, medals).
- accessibility.colorblind_mode ∈ {"off", "deuteranopia", "protanopia", "tritanopia"}.
- accessibility.subtitle_size ∈ {"small", "medium", "large", "x-large"}.
- privacy.allow_dms ∈ {"everyone", "team_only", "none"}.
- medal_privacy.visibility ∈ {"all", "curated", "none"}.
- medal_privacy.pinned_medal_id and entries in curated_medal_ids / unviewed_awards reference Medal.id (string PK on the medals table — see ../FEATURES/gameplay/medals.md).
- audio.*_volume clamped to [0.0, 1.0].
Status: —
ui/audio/accessibility/privacy/gameplayblocks are the target schema; the live model only enforcesnotification_preferencesandbountiestoday.
Player.first_login¶
{
"completed": bool,
"session_id": str,
"ship_chosen": str,
"completed_at": iso8601
}
Defaults: {"completed": false}.
Constraints: Once completed flips to true it must not flip back. Admin reset goes through DELETE /first-login/session, which resets the entire row, not this column.
Player.insurance¶
{
"type": "NONE | BASIC | STANDARD | PREMIUM",
"purchased_at": iso8601,
"expires_at": iso8601,
"claims_remaining": int
}
Defaults: null — the column is nullable; an absent row means no insurance.
Constraints: type must match the InsuranceType enum in services/gameserver/src/models/ship.py.
Ship¶
Source: services/gameserver/src/models/ship.py
Ship.maintenance¶
{
"current_integrity": float,
"max_integrity": float,
"last_serviced_at": iso8601,
"decay_rate_per_turn": float,
"next_failure_check_at": iso8601,
"failure_history": [
{"type": "MINOR | MAJOR | CATASTROPHIC", "at": iso8601, "repaired_at": iso8601}
]
}
Defaults: Set on ship creation by services/gameserver/src/services/ship_service.py from the matching ShipSpecification.
Constraints: current_integrity must be in [0, max_integrity]. When it hits 0, the ship is destroyed (is_destroyed = true).
Ship.cargo¶
Live shape (seeded by first_login_service / NPC traders; mutated by contract, NPC trade, contraband, construction, fuel-delivery, beacon, abandonment paths):
{
"capacity": int,
"used": int,
"contents": {
"ore": int,
"organics": int,
"gourmet_food": int,
"fuel": int,
"equipment": int,
"exotic_technology": int,
"luxury_goods": int,
"colonists": int,
"precious_metals": int,
"lumen_crystals": int,
"quantum_shards": int
},
"_capacity_bonus_percent": float
}
Canonical keys (2026-08-05): capacity / used / contents — not max_capacity / used_capacity, and not top-level commodity keys. _capacity_bonus_percent is an optional meta-key written by ShipUpgradeService (Cargo-Hold); readers must use effective_cargo_capacity(ship) rather than raw capacity when applying the bonus.
Defaults: {capacity: ShipSpecification.max_cargo, used: 0, contents: {}} on creation.
Constraints: used ≤ capacity (before bonus). used must equal sum(contents.values()). Commodity keys inside contents match the canonical commodity catalog at ../FEATURES/definitions.md#resource-types plus colonists.
Known legacy writer: ~~enhanced_websocket_service still mutates a flat {commodity: qty} map on some websocket trade paths.~~ Fixed 2026-08-05 (WO-FIX-ENHANCED-WEBSOCKET-CARGO-SHAPE-MISMATCH) — websocket buy/sell now writes nested contents + used/capacity and flag_modified, matching REST trading.py.
Mining-drop slots (per ADR-0065 M-E1): lumen_crystals and quantum_shards live under contents. Both are integer counts; default absent/0; produced as mining drops per ../FEATURES/economy/mining.md; extracted at TradeDocks for refining (per ADR-0009).
Ship.combat¶
{
"attack_rating": int,
"defense_rating": int,
"shields_current": int,
"shields_max": int,
"shields_recharge_rate": float,
"shield_resistance": float,
"armor_rating": float,
"evasion": int,
"weapons": [
{"type": str, "damage": int, "range": int, "ammo": int}
]
}
Defaults: Populated from ShipSpecification at creation time. shield_resistance and armor_rating default to 0.0 (no damage absorbed); ship class and upgrades raise them — see the canonical damage stack in ../SYSTEMS/combat-resolver.md#damage-stack-order-of-operations.
Constraints:
- shields_current ≤ shields_max.
- shield_resistance ∈ [0.0, 1.0] — fraction of shield-damage absorbed (0.0 = no resistance, 1.0 = total absorption).
- armor_rating ∈ [0.0, 1.0] — fraction of hull-damage absorbed (post-shield residual). Same units as shield_resistance for symmetry.
Ship.upgrades¶
[
{
"type": "ENGINE | CARGO_HOLD | SHIELD | HULL | SENSOR | DRONE_BAY | GENESIS_CONTAINMENT | MAINTENANCE_SYSTEM",
"level": int,
"installed_at": iso8601,
"credits_spent": int
}
]
Defaults: [].
Constraints: At most one entry per type. level must be ≤ ShipSpecification.max_upgrade_levels[type].
Ship.equipment_slots¶
{
"<slot_name>": {
"equipment_id": str,
"installed_at": iso8601,
"condition": float
}
}
Defaults: {}.
Constraints: Slot names are validated against ShipSpecification.special_abilities.
Ship.insurance¶
Same shape as Player.insurance. null when uninsured.
ShipSpecification.max_upgrade_levels¶
{
"ENGINE": int,
"CARGO_HOLD": int,
"SHIELD": int,
"HULL": int,
"SENSOR": int,
"DRONE_BAY": int,
"GENESIS_CONTAINMENT": int,
"MAINTENANCE_SYSTEM": int
}
Defaults: Required; populated from the ship-spec seed data.
Constraints: Keys must match the UpgradeType enum.
ShipSpecification.special_abilities¶
[
{"name": str, "description": str, "slot": str, "compatible_with": [str]}
]
Defaults: [].
ShipSpecification.acquisition_methods¶
[
{"method": "purchase | quest_reward | faction_unlock | event_drop", "cost": int, "requirement": str}
]
Defaults: [].
ShipSpecification.faction_requirements¶
{
"<faction_code>": {"min_reputation": int, "tier": str}
}
Defaults: null (no requirement).
Sector¶
Source: services/gameserver/src/models/sector.py
Sector.resources¶
{
"has_asteroids": bool,
"asteroid_yield": {
"ore": int,
"precious_metals": int,
"quantum_shards": int
},
"gas_clouds": [
{"type": str, "density": int}
],
"has_scanned": bool
}
Defaults:
{
"has_asteroids": false,
"asteroid_yield": {"ore": 0, "precious_metals": 0, "quantum_shards": 0},
"gas_clouds": [],
"has_scanned": false
}
Constraints: asteroid_yield values are non-negative. Live key is quantum_shards (bang SectorResources / gameserver harvest contract); radioactives is a retired slug and must not appear as current schema.
Sector.players_present¶
[
{
"player_id": str,
"ship_id": str,
"ship_type": str,
"joined_at": iso8601,
"last_seen": iso8601,
"is_cloaked": bool
}
]
Defaults: [].
Constraints: Maintained by the movement service; player_id must be a valid Player.id UUID. The fat-dict shape supports presence-aware features (sector-presence encounter checks per SYSTEMS/sector-presence.md). Stale entries are cleaned up when a player moves out or last_seen exceeds the presence-decay window. 📐 is_cloaked is a schema placeholder only — zero gameserver writers/readers as of 2026-08-04; do not treat it as live cloaking. Presence-hide design pending Max ruling (same DECISION as Ship.has_cloaking).
Sector.ships_present¶
[
{"ship_id": str, "owner_id": str, "type": str, "is_cloaked": bool}
]
Defaults: [].
Constraints: Mirrors Ships rows whose sector_id equals this sector's sector_id. The JSONB is the read-fast denormalization; the relational column is canonical.
Sector.defenses¶
{
"drone_blocks": [
{
"count": int,
"owner_player_id": str,
"owner_team_id": "str | null",
"faction_code": "str | null",
"deployed_at": iso8601,
"is_hostile_to_passers": bool,
"weapon_profile": "attack | defense"
}
],
"dominant_owner_id": "uuid | null",
"dominant_owner_name": "str | null",
"dominant_team_id": "uuid | null",
"mines": int,
"mine_owner_id": "uuid | null",
"police_patrol_ships": [
{
"patrol_id": str,
"faction_code": str,
"squad_kind": "frontier_militia | corporate_security | terran_navy | cabal_skirmisher | mercenary_company | federation_marshal | nexus_sentinel",
"npc_character_ids": [str],
"ship_count": int,
"wanted_threshold": int,
"deployed_at": iso8601,
"scheduled_clear_at": "iso8601 | null",
"shift_handoff_state": {
"outgoing_npc_id": "str | null",
"incoming_npc_id": "str | null",
"overlap_started_at": "iso8601 | null",
"threat_notes": [{"player_id": str, "sector_id": int, "note": str, "timestamp": iso8601}],
"engaged_player_ids": [str],
"coverage_gap_started_at": "iso8601 | null"
}
}
],
"pirate_patrol_ships": [
{
"patrol_id": str,
"faction_code": str,
"squad_kind": str,
"npc_character_ids": [str],
"ship_count": int,
"deployed_at": iso8601,
"scheduled_clear_at": "iso8601 | null"
}
],
"docked_npc_ships": [
{
"npc_id": str,
"ship_id": str,
"status": "docked_off_duty | docked_maintenance | docked_standby",
"docked_at": iso8601
}
]
}
Defaults:
{
"drone_blocks": [],
"dominant_owner_id": null,
"dominant_owner_name": null,
"dominant_team_id": null,
"mines": 0,
"mine_owner_id": null,
"police_patrol_ships": [],
"pirate_patrol_ships": [],
"docked_npc_ships": []
}
Shipped as two dedicated keys, not a single patrol_ships list. police_patrol_ships (Federation Marshal / Nexus Sentinel squads) and pirate_patrol_ships (ADR-0047 pirate rosters) are separate lists rather than one unified patrol_ships key — src/services/npc_spawn_service.py's POLICE_PATROL_DEFENSES_KEY / PIRATE_PATROL_DEFENSES_KEY. patrol_ships itself is not a Sector.defenses key — it's an unrelated key on Station.defenses (see below), a per-station scalar-int garrison count that never collides with these lists (different model), but the split was chosen defensively under the belief that it would. This is the shipped mechanism movement_service.py's sector-entry encounter check and npc_engagement_service.py's ADR-0042 dispatch/sweep engine are built against.
docked_npc_ships semantics. Off-duty NPC ships parked at this sector (typically the host sector of an NPCBarracks or OutlawBase). The combat-resolver checks this array during attack-validation: ships listed with status = "docked_off_duty" at a sector flagged as the NPC's home_barracks_id host are shielded from player attack (ERR_NPC_SHIP_AT_BARRACKS). Ships at docked_standby (e.g., a pirate captain's ship parked at a public Fringe-Alliance port that isn't his outlaw base) are not shielded — players who infiltrate the port can attack them. docked_maintenance is a middle state — auto-repair queued during the NPC's sleep block; ships in this state are also shielded as a side effect of being at a barracks.
dominant_* fields are denormalized read-side conveniences for the UI — derived from the largest drone_blocks entry. Multiple players can stack defenses in the same sector via separate drone_blocks entries; each block tracks its own owner, team, faction (if NPC patrol), deploy timestamp, hostility flag (drives whether it engages passers-by or only attackers), and weapon profile.
police_patrol_ships / pirate_patrol_ships are NPC-only per SD2 — these are faction patrol squads spawned by the world generator and zone-policing system, never player-deployable. Each entry carries faction_code and squad_kind so sector-presence._check_for_encounters can compare a passing ship's Wanted status (Player.personal_reputation and any active stolen-ship reports) against the patrol's wanted_threshold to decide whether to fire pursuit (pirate_patrol_ships entries carry no wanted_threshold — pirate aggression is unconditional presence-based, not a Wanted-status gate). scheduled_clear_at is non-NULL on temporary surge patrols (e.g., zone-event spawned squads); NULL means the patrol persists until the sector's policing rules say otherwise. Player-deployable defenses live in drone_blocks, not here.
Constraints:
- When any drone_blocks[].count > 0 or mines > 0, the corresponding owner field must not be null.
- police_patrol_ships[].faction_code / pirate_patrol_ships[].faction_code must reference a valid faction (terran_federation, cabal, corporate, frontier, etc.); never NULL.
- police_patrol_ships[].owner_player_id / pirate_patrol_ships[].owner_player_id is disallowed — the schema enforces NPC-only by omitting the field entirely.
Sector.active_events¶
[
{
"event_id": str,
"type": str,
"started_at": iso8601,
"expires_at": iso8601,
"payload": {}
}
]
Defaults: [].
Sector.nav_hazards¶
{
"<hazard_type>": {
"severity": int,
"duration_remaining": int
}
}
Defaults: {}.
Sector.nav_beacons¶
[
{"beacon_id": str, "owner_id": str, "message": str, "placed_at": iso8601}
]
Defaults: [].
Planet¶
Source: services/gameserver/src/models/planet.py
Planet.resources¶
{
"fuel_ore": int,
"organics": int,
"equipment": int,
"rare_minerals": int,
"extraction_modifier": float
}
Defaults: {}.
Constraints: Quantities are non-negative. Specialization can boost extraction_modifier.
Planet.economy¶
{
"trade_balance": int,
"tax_rate": float,
"currency_reserves": int,
"active_treaties": [str],
"trade_routes": [
{"partner_planet_id": str, "volume": int, "established_at": iso8601}
]
}
Defaults: {}.
Constraints: tax_rate in [0.0, 1.0].
Planet.production¶
{
"fuel": int,
"organics": int,
"equipment": int,
"research": int
}
Defaults: {"fuel": 0, "organics": 0, "equipment": 0, "research": 0}.
Constraints: Each value is non-negative; the per-tick rate is computed by _calculate_production_rates in services/gameserver/src/services/planetary_service.py.
Planet.active_events¶
Same shape as Sector.active_events.
Galaxy¶
Source: services/gameserver/src/models/galaxy.py
Galaxy.statistics¶
{
"total_sectors": int,
"discovered_sectors": int,
"station_count": int,
"planet_count": int,
"player_count": int,
"team_count": int,
"warp_tunnel_count": int,
"genesis_count": int
}
Defaults: All zeros.
Constraints: Counts are non-negative; discovered_sectors ≤ total_sectors. Maintained by Galaxy.update_statistics().
Galaxy.density¶
{
"station_density": int,
"planet_density": int,
"one_way_warp_percentage": int,
"resource_distribution": {
"ore": int,
"organics": int,
"equipment": int,
"luxury_goods": int,
"medical_supplies": int,
"technology": int
}
}
Defaults: station_density=10, planet_density=3, one_way_warp_percentage=5, distribution shown in source.
Constraints: Density percentages are validated against the canonical ranges (5–15 for stations, 10–25 for planets, ~10 for one-way warps). resource_distribution values should sum to 100.
Galaxy.faction_influence¶
{
"terran_federation": int,
"mercantile_guild": int,
"frontier_coalition": int,
"astral_mining_consortium": int,
"nova_scientific_institute": int,
"fringe_alliance": int,
"player_controlled": int,
"contested": int
}
Defaults: Shown in source.
Constraints: Values sum to 100.
Galaxy.state¶
{
"age_in_days": int,
"resource_depletion": int,
"economic_health": int,
"exploration_percentage": int,
"player_wealth_distribution": {
"top_10_percent": float,
"middle_40_percent": float,
"bottom_50_percent": float
}
}
Defaults: Shown in source.
Constraints: Wealth distribution percentages sum to 100.
Galaxy.events¶
{
"active_events": [
{"event_id": str, "started_at": iso8601, "expires_at": iso8601}
],
"scheduled_events": [
{"event_id": str, "scheduled_for": iso8601}
]
}
Defaults: {"active_events": [], "scheduled_events": []}.
Galaxy.combat_penalties¶
{
"federation": "high | medium | low | none",
"border": "high | medium | low | none",
"frontier": "high | medium | low | none"
}
Defaults: {"federation": "high", "border": "medium", "frontier": "none"}.
Galaxy.economic_modifiers¶
{
"global_price_multiplier": float,
"<commodity_key>": {"multiplier": float, "expires_at": iso8601}
}
Defaults: {}.
Cluster¶
Source: services/gameserver/src/models/cluster.py
Cluster.stats¶
{
"total_sectors": int,
"populated_sectors": int,
"empty_sectors": int,
"resource_value": int,
"danger_level": int,
"development_index": int,
"exploration_percentage": int
}
Defaults: Shown in source. All numeric fields are 0-100.
Cluster.resources¶
{
"primary_resources": [str],
"resource_distribution": {
"ore": int,
"organics": int,
"equipment": int,
"luxury_goods": int,
"medical_supplies": int,
"technology": int
},
"special_resources": [str]
}
Defaults: Shown in source. resource_distribution sums to 100.
Cluster.faction_influence¶
{
"terran_federation": int,
"mercantile_guild": int,
"frontier_coalition": int,
"astral_mining_consortium": int,
"nova_scientific_institute": int,
"fringe_alliance": int,
"dominant_faction": str
}
Defaults: Shown in source.
Constraints: Numeric values sum to 100; dominant_faction is the key with the highest value. Roster: only the six "territorial" allyable factions appear in cluster influence — Terran Federation, Mercantile Guild, Frontier Coalition, Astral Mining Consortium, Nova Scientific Institute, Fringe Alliance. Shadow Syndicate is intentionally excluded because per FEATURES/gameplay/faction-lore.md Syndicate territory is operational (it hides inside other factions' space) rather than geographical, so cluster-level influence percentages don't apply. Pirates and Cabal are hostile-only and similarly absent — Pirates have no fixed territory; Cabal occupies a single Terran-Space HQ sector (Border Zone, randomized 101–200 per ../FEATURES/gameplay/faction-lore.md#the-cabal) without cluster-level influence.
Cluster.resource_modifiers¶
{
"<resource_key>": float
}
Defaults: {}. Each value is a multiplier applied to extraction in this cluster.
Cluster.discovery_requirement¶
{
"min_player_rank": str,
"min_sectors_explored": int,
"required_quest": str
}
Defaults: null (no requirement, cluster starts visible).
WarpTunnel¶
Source: services/gameserver/src/models/warp_tunnel.py
WarpTunnel.properties¶
{
"length": float,
"stability_rating": int,
"expected_lifetime": "iso8601 | null",
"age": int,
"traversal_cost": int,
"cool_down": int,
"discovered": bool,
"discoverer_id": "uuid | null",
"discovery_date": "iso8601 | null",
"affected_by_storms": bool
}
Defaults: Shown in source. Natural tunnels have expected_lifetime: null; artificial tunnels carry an explicit collapse time.
WarpTunnel.tunnel_status¶
{
"is_active": bool,
"disruption": "null | { reason: str, until: iso8601 }",
"traffic_level": int,
"last_traversal": "iso8601 | null",
"maintenance_status": "null | { state: str, scheduled_for: iso8601 }"
}
Defaults: Shown in source.
WarpTunnel.source_endpoint / destination_endpoint¶
Same shape on both:
{
"sector_id": "uuid | null",
"cluster_id": "uuid | null",
"region_id": "uuid | null",
"coordinates": {"x": int, "y": int, "z": int},
"controlling_faction": "str | null",
"is_secured": bool,
"access_requirements": "null | { min_rank: str, min_reputation: int }"
}
Defaults: Shown in source.
Constraints: sector_id, cluster_id, region_id should be populated when the endpoint is real. access_requirements is null on public tunnels.
WarpTunnel.artificial_data¶
{
"creator_id": str,
"created_at": iso8601,
"fuel_cost": int,
"decay_per_day": float,
"anchor_method": str,
"owner_team_id": "uuid | null"
}
Defaults: null (only populated for type = ARTIFICIAL).
WarpTunnel.traversal_history¶
[
{"ship_id": str, "owner_id": str, "at": iso8601, "direction": "outgoing | incoming"}
]
Defaults: []. Capped to the last 100 entries; older entries are evicted by the traversal handler.
WarpTunnel.access_requirements¶
{
"min_rank": str,
"min_reputation": int,
"required_faction": str,
"required_quest": str
}
Defaults: null.
WarpTunnel.special_effects¶
{
"<effect_name>": {"magnitude": float, "duration_turns": int}
}
Defaults: {}.
SpecialFormation¶
Source: services/gameserver/src/models/special_formation.py
SpecialFormation.properties¶
Type-specific generation parameters. The shape varies by the row's SpecialFormationType; keys present in a row must match the row's type. A BUBBLE row never carries target_formation_id; a BACKDOOR row never carries interior_size.
{
// BUBBLE / DEAD_END_BUBBLE
"interior_size": int,
"link_tunnel_depth": int,
"branching": "chain | tree | cyclic",
// GOLD_BUBBLE
"gateway_count": int,
"interior_size_min": int,
// TUNNEL
"length": int,
"one_way_bias": float,
// DEAD_END
"parent_kind": "open | tunnel | bubble",
"is_unfigged_only": bool,
// WARP_SINK
"entry_count": int,
"recovery_method": "slipdrive | distress | scheduled_clear | none",
// BACKDOOR
"target_formation_id": str,
"entry_distance": int,
// BLISTER
"interior_size": int,
"bypass_distance": int,
// ESCAPE_HATCH
"surprise_source_distance": int,
// LOST_SECTOR
"exitWarp": null | { "destinationSectorId": int },
// LOST_CLUSTER
"interiorClusterId": str,
"exitWarp": null | { "sourceSectorId": int, "destinationSectorId": int },
// ARCHIPELAGO
"memberClusterIds": [str],
"crossWarpCount": int,
"exitWarps": [{ "sourceSectorId": int, "destinationSectorId": int }]
}
Defaults: {} — only the keys relevant to the row's type are populated by the generator.
Constraints:
- interior_size, length, entry_count, gateway_count, interior_size_min, entry_distance, bypass_distance, surprise_source_distance are positive integers.
- link_tunnel_depth ≥ 0.
- one_way_bias ∈ [0.0, 1.0].
- branching ∈ {"chain", "tree", "cyclic"}.
- parent_kind ∈ {"open", "tunnel", "bubble"}.
- recovery_method ∈ {"slipdrive", "distress", "scheduled_clear", "none"}. WARP_SINK rows set this to a non-"none" value unless they are deliberate traps.
- target_formation_id for BACKDOOR rows references a SpecialFormation.id that exists in the same region.
- entry_distance and surprise_source_distance are graph hops; min 1, max 32 (typical region diameter).
- gateway_count for GOLD_BUBBLE ∈ [1, 3].
- exitWarp on LOST_SECTOR and LOST_CLUSTER is either null or an object — never an array. destinationSectorId must reference a sector in the main connected sub-graph (not inside any lost formation and not in fedspace).
- exitWarps on ARCHIPELAGO is an array (may be empty); each entry's sourceSectorId must belong to a different member cluster (per ADR-0070).
- interiorClusterId (LOST_CLUSTER) references a Cluster row whose sector set equals the formation's interior_sector_ids. memberClusterIds (ARCHIPELAGO) references 2–6 Cluster rows whose union of sector sets equals the formation's interior_sector_ids.
SpecialFormation.discovery_requirement¶
{
"ship_class": str,
"min_reputation": {"<faction_code>": int},
"warp_jumper_required": bool,
"min_player_rank": str
}
Defaults: null — most formations are discoverable on first visit and scan with no gating.
Constraints: All keys optional. The discovery service evaluates each present key against the visiting player; a missing key means the requirement does not apply. Used for GOLD_BUBBLE story sites and other operator-placed rare formations.
Station¶
Source: services/gameserver/src/models/station.py
Station.commodities¶
{
"<commodity_key>": {
"quantity": int,
"capacity": int,
"base_price": int,
"current_price": int,
"production_rate": int,
"price_variance": int,
"buys": bool,
"sells": bool
}
}
Defaults: Shown in source — eight commodity keys populated with class-appropriate defaults.
Constraints: quantity ≤ capacity. current_price is recomputed by the market service on each price-update tick.
Station.trader_personality¶
Drives haggling behavior for the station's NPC trader: difficulty, preferred persuasion approach, memory of past interactions, and per-player trust.
{
"type": "Federation | Border | Frontier | Luxury | Black Market",
"haggling_difficulty": int,
"preferred_appeal_types": [str],
"memory_duration_days": int,
"trust_level": int,
"quirks": [str]
}
Defaults: Populated at station creation from the personality archetype keyed off the station's class. Archetype defaults:
type |
haggling_difficulty |
preferred_appeal_types |
memory_duration_days |
|---|---|---|---|
| Federation | 3 | procedural, compliance |
14 |
| Border | 5 | economic, personal |
30 |
| Frontier | 7 | personal, risk |
60 |
| Luxury | 8 | cultural, aesthetic |
21 |
| Black Market | 9 | risk, discretion |
90 |
trust_level defaults to 0; quirks defaults to [].
Constraints:
- type must be one of the five archetypes above.
- haggling_difficulty is in [1, 10]; higher values are harder to haggle against.
- preferred_appeal_types entries are drawn from procedural, compliance, economic, personal, cultural, aesthetic, risk, discretion. A player using a preferred appeal type receives a +20% success modifier (see ../FEATURES/economy/haggling.md).
- memory_duration_days is in [7, 90] and controls how long the station remembers a given player's haggling history.
- trust_level is in [-1000, 1000], accumulated per player via repeated successful trades and eroded by failed haggling or hostile actions; high trust eases haggling difficulty.
- quirks are free-form behavior modifiers consumed by the haggling resolver. Examples: "always_haggles_under_500_units", "responds_to_humor", "refuses_pirates", "prefers_carrier_owners".
Station.services¶
{
"ship_dealer": bool,
"ship_repair": bool,
"ship_maintenance": bool,
"ship_upgrades": bool,
"insurance": bool,
"drone_shop": bool,
"genesis_dealer": bool,
"mine_dealer": bool,
"diplomatic_services": bool,
"storage_rental": bool,
"market_intelligence": bool,
"refining_facility": bool,
"luxury_amenities": bool
}
Defaults: Shown in source.
Station.defenses¶
{
"defense_drones": int,
"max_defense_drones": int,
"auto_turrets": bool,
"defense_grid": bool,
"shield_strength": int,
"patrol_ships": int,
"military_contract": bool
}
Defaults: Shown in source.
Station.ownership¶
{
"owner_id": str,
"owner_type": "player | team | syndicate | faction",
"acquired_at": iso8601,
"acquisition_price": int,
"acquisition_method": "purchase | trade_volume | faction_standing | takeover",
"tariff_rate": float,
"docking_fee": int,
"service_charge_multiplier": float,
"price_adjustment_pct": float,
"preferred_traders": [str],
"shareholders": [
{
"player_id": str,
"stake_pct": float,
"joined_at": iso8601,
"voting_weight": float
}
],
"treasury_balance": int,
"treasury_pending_payouts": int,
"withdrawal_schedule": "monthly | quarterly | manual",
"upgrade_levels": {
"extended_storage": int,
"market_intelligence": int,
"automated_trading": int,
"shipyard": int,
"refining": int,
"luxury_amenities": int,
"defense_grid": int,
"security_patrol": bool,
"military_contract": iso8601
},
"lifetime_revenue": int,
"monthly_revenue_history": [
{
"period": "YYYY-MM",
"tariff_revenue": int,
"docking_revenue": int,
"service_revenue": int,
"storage_revenue": int,
"info_revenue": int,
"maintenance_costs": int,
"net_profit": int
}
]
}
Defaults: null until first acquired. Field semantics:
tariff_rate— 0.02–0.08 (range 2%–8%); applied to every player buy/sell at the station.docking_fee— 50–500 credits per docking; flat charge, owner-set.service_charge_multiplier— 0.8–2.0 (×0.8 loss-leader to ×2.0 premium); applied to repair/refueling/drone/refining services.price_adjustment_pct— −0.10 to +0.10 (−10% to +10%); owner can nudge buy and sell prices within this band, applied after the standard supply/demand formula.shareholders— forowner_type = syndicate; max 10 stakeholders; sum ofstake_pct≤ 100; voting threshold 50% by default. See../FEATURES/economy/port-ownership.md#syndicate.withdrawal_schedule— when treasury auto-distributes profits to owner / shareholders.upgrade_levels— integer level per upgrade type (Extended Storage L1-3, Market Intelligence L1-3, Auto Trading L1-3, Shipyard L1-3, Refining L1-3, Luxury Amenities L1-3, Defense Grid L1-3); booleans for Security Patrol (active subscription) and Military Contract (binding-until timestamp).monthly_revenue_history— rolling 12-month accounting; older periods rolled intolifetime_revenue.
Status: — Station ownership ships only the 5-field stub at launch (owner_id, owner_type, acquired_at, acquisition_price, tariff_rate). The full shape above is the launch-target home for syndicate stakes, treasury accounting, upgrade levels, and price adjustments described in ../FEATURES/economy/port-ownership.md.
Station.acquisition_requirements¶
{
"min_trade_volume": int,
"min_faction_standing": str,
"base_price": int
}
Defaults: Shown in source.
Station.price_modifiers, service_prices, active_events¶
price_modifiers and service_prices are owner-set {<key>: int} maps; default {}. active_events follows the same shape as Sector.active_events.
Reputation (Faction reputation cache)¶
Source: services/gameserver/src/models/reputation.py
Reputation.history (player-faction)¶
[
{"delta": int, "reason": str, "at": iso8601, "source_event": str, "sector_id": "uuid | null"}
]
Defaults: []. Capped at the most-recent 100 entries. sector_id is the Sector.id where the triggering event occurred (combat, port transaction, sector-defense event); null for non-spatial events (admin grant, decay tick, server-initiated).
TeamReputation.faction_reputation¶
{"<faction_code>": int}
Defaults: {}.
TeamReputation.history¶
Same shape as Reputation.history.
TeamReputation.pending_notifications¶
[
{"id": str, "type": str, "message": str, "created_at": iso8601}
]
Defaults: [].
Team¶
Source: services/gameserver/src/models/team.py
Team.join_requirements¶
{
"min_player_rank": str,
"min_personal_reputation": int,
"required_faction_standing": {"<faction_code>": int},
"invitation_only": bool
}
Defaults: {}.
Team.member_roles¶
{
"<player_uuid>": {"role": str, "since": iso8601}
}
Defaults: {}.
Team.resource_sharing¶
{
"auto_share_credits": bool,
"auto_share_cargo": bool,
"share_percent": int
}
Defaults: {}.
Team.invitation_codes¶
[
{"code": str, "created_by": str, "created_at": iso8601, "expires_at": iso8601, "max_uses": int, "uses": int}
]
Defaults: [].
TeamMember.permissions¶
TeamMember has explicit boolean columns (can_invite, can_kick, can_manage_treasury, can_manage_alliances) for the canonical permission set; the permissions JSONB is a free-form extension surface for per-team overrides and future flags. Recommended shape mirrors the column names so role-resolution can fall back uniformly:
{
"can_invite": bool,
"can_kick": bool,
"can_manage_treasury": bool,
"can_manage_alliances": bool,
"<custom_flag>": bool
}
Defaults: {}. Custom flags are scoped to the owning team and have no game-side meaning unless team-specific code reads them.
TeamMember.contribution_credits¶
{
"credits_donated": int,
"resources_donated": {"<commodity_key>": int},
"ships_destroyed": int,
"contracts_completed": int
}
Defaults: {}.
Other notable JSONB columns¶
DroneandDroneDeployment— seeservices/gameserver/src/models/drone.py. No JSONB columns; deployment state is fully relational.Fleet.battle_log(JSON) — append-only log of round-by-round battle events.[]default.Fleet.resources_looted(JSON) —{<commodity_key>: int}, default{}.MarketTransaction— relational, no JSONB.AIRecommendation.recommendation_data,AIRecommendation.contextual_data— opaque payload owned by the AI service. Seeservices/gameserver/src/services/ai_trading_service.pyfor shape; treat as service-private.ARIA*models —services/gameserver/src/models/aria_personal_intelligence.py. MultipleJSONcolumns documented inline in the model file; treat their shape as private to the ARIA service.Region.language_pack,Region.aesthetic_theme,Region.traditions,Region.social_hierarchy,Region.trade_bonuses— seeOPERATIONS/multi-regional.md. Each is owner-controlled metadata used to differentiate regions; the gameserver does not validate inner shapes.RegionalElection.candidates,RegionalElection.results— election scaffolding documented inOPERATIONS/multi-regional.md.
Migrations¶
JSONB shape changes go through Alembic migrations under services/gameserver/alembic/versions/. Adding a key is non-breaking; renaming or removing a key requires a backfill step. Reads that depend on a key existing should default-fall-back rather than assume the row was migrated.