JSONB schemas¶
Most domain models lean on PostgreSQL JSONB columns rather than fully-relational sub-tables. This page documents the target shape of each JSONB column so consumers can write to it without guessing.
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
},
"bounties": [
{
"id": str,
"placer_id": str,
"amount": int,
"placed_at": iso8601,
"reason": 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).
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).
- accessibility.colorblind_mode ∈ {"off", "deuteranopia", "protanopia", "tritanopia"}.
- accessibility.subtitle_size ∈ {"small", "medium", "large", "x-large"}.
- privacy.allow_dms ∈ {"everyone", "team_only", "none"}.
- audio.*_volume clamped to [0.0, 1.0].
Status: 📐 Design-only —
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¶
{
"fuel_ore": int,
"organics": int,
"equipment": int,
"luxury_goods": int,
"medical_supplies": int,
"technology": int,
"colonists": int,
"max_capacity": int,
"used_capacity": int
}
Defaults: Set on creation from ShipSpecification.max_cargo. All commodity slots default to 0.
Constraints: used_capacity ≤ max_capacity. The trading service is responsible for keeping the sum of commodity quantities equal to used_capacity.
Ship.combat¶
{
"attack_rating": int,
"defense_rating": int,
"shields_current": int,
"shields_max": int,
"shields_recharge_rate": float,
"evasion": int,
"weapons": [
{"type": str, "damage": int, "range": int, "ammo": int}
]
}
Defaults: Populated from ShipSpecification at creation time.
Constraints: shields_current ≤ shields_max.
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,
"radioactives": int
},
"gas_clouds": [
{"type": str, "density": int}
],
"has_scanned": bool
}
Defaults:
{
"has_asteroids": false,
"asteroid_yield": {"ore": 0, "precious_metals": 0, "radioactives": 0},
"gas_clouds": [],
"has_scanned": false
}
Constraints: asteroid_yield values are non-negative.
Sector.players_present¶
[ "<player_uuid>", "<player_uuid>" ]
Defaults: [].
Constraints: Maintained by the movement service; entries must be valid Player.id UUIDs. Stale entries are cleaned up when a player moves out.
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¶
{
"defense_drones": int,
"owner_id": "uuid | null",
"owner_name": "str | null",
"team_id": "uuid | null",
"mines": int,
"mine_owner_id": "uuid | null",
"patrol_ships": [
{"ship_id": str, "owner_id": str}
]
}
Defaults:
{
"defense_drones": 0,
"owner_id": null,
"owner_name": null,
"team_id": null,
"mines": 0,
"mine_owner_id": null,
"patrol_ships": []
}
Constraints: When defense_drones > 0 or mines > 0, the corresponding owner field must not be null.
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 data spec ranges (5-15 for stations, 2-5 for planets, 2-8 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.
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: {}.
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 |
30 |
| Border | 5 | economic, personal |
30 |
| Frontier | 7 | personal, risk |
14 |
| Luxury | 8 | cultural, aesthetic |
60 |
| Black Market | 9 | risk, discretion |
7 |
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 | faction",
"acquired_at": iso8601,
"acquisition_price": int,
"tax_rate": float
}
Defaults: null until first acquired.
Station.acquisition_requirements¶
{
"min_trade_volume": int,
"min_faction_standing": str,
"base_price": int,
"special_missions": [str]
}
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}
]
Defaults: []. Capped at the most-recent 100 entries.
Reputation.mission_availability¶
[
{"mission_id": str, "unlocked_at": iso8601, "expires_at": "iso8601 | null"}
]
Defaults: [].
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¶
{
"can_invite": bool,
"can_kick": bool,
"can_modify_treasury": bool,
"can_declare_war": bool,
"can_modify_team_settings": bool
}
Defaults: {}.
TeamMember.contribution_credits¶
{
"credits_donated": int,
"resources_donated": {"<commodity_key>": int},
"ships_destroyed": int,
"missions_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.