Skip to content

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, or iso8601. 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"; null means 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 / gameplay blocks are the target schema; the live model only enforces notification_preferences and bounties today.

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

  • Drone and DroneDeployment — see services/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. See services/gameserver/src/services/ai_trading_service.py for shape; treat as service-private.
  • ARIA* models — services/gameserver/src/models/aria_personal_intelligence.py. Multiple JSON columns 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 — see OPERATIONS/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 in OPERATIONS/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.