Player Domain¶
Status: 🚧 Partial — Player, User, Team, Reputation, FirstLogin model family are committed; 10 design-only Player fields (ARIA trust/abuse, suspect/wanted flags … (impl audit 2026-06-16)
User accounts, the in-game Player row, the first-login onboarding flow, teams, reputation, and messaging.
Schema status¶
Per ADR-0066 D-V1, schema-level implementation status is consolidated here. Field descriptions describe the target schema.
Design-only columns — present in the spec, not yet committed:
Player:nickname,aria_trust_score,aria_blocked_until,aria_violation_count,suspect_status,suspect_until,suspect_team_snapshot,wanted_status,wanted_until,daily_faction_rep_consumed,daily_faction_rep_consumed_date.PlayerTradingProfile,AIRecommendation: model classes exist; columns above are the launch target.PlayerCentralBankAccount,TakeoverIntent,PlayerWarpKnowledge: entire models are design-only — schema below describes the launch target.
All other columns on this page are committed.
User¶
Source: services/gameserver/src/models/user.py
Purpose: Authentication-layer account. One User may own one Player (game state) and/or AdminCredentials. Subscription / billing flags live here.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| username | String(50) | unique, not null | |
| String(255) | unique, nullable | optional | |
| is_active | Boolean | default true | |
| is_admin | Boolean | default false | |
| last_login | DateTime | nullable | |
| deleted | Boolean | default false | soft delete |
| paypal_subscription_id, subscription_tier, subscription_status | String | nullable | galactic_citizen etc. |
| subscription_started_at, subscription_expires_at | TIMESTAMP | nullable |
Relationships:
- player (1:1, cascade), admin_credentials (1:1, cascade), player_credentials (1:1, cascade), mfa_secret (1:1, cascade).
- oauth_accounts, refresh_tokens, mfa_attempts (1:many).
- owned_regions → Region.
Player¶
Source: services/gameserver/src/models/player.py
Purpose: In-game state for a user — credits, turns, reputation snapshot, location, ship pointer, ARIA AI counters, and regional citizenship.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| user_id | UUID FK users.id | unique, not null, CASCADE | 1:1 with User |
| nickname | String(50) | nullable, unique index | Optional in-game callsign. Set via the first-login confirmation step (see ../SYSTEMS/first-login.md#completion-side-effects-complete_first_login) — gated on player Yes/No after extraction-and-validation; 3–20 chars; alphanumeric + _ + - + single internal space; profanity- and impersonation-filtered; unique across all Players. NULL means no callsign set; UI falls back to User.username. |
| credits | Integer | default 10000 | |
| turns | Integer | default 1000 | |
| reputation | JSONB | default {} | per-faction reputation snapshot |
| personal_reputation | Integer | default 0 | -1000..+1000 alignment (migration fe22441146b1) |
| reputation_tier | String(50) | default Neutral |
cached tier name |
| name_color | String(20) | default #FFFFFF |
UI color cache |
| military_rank | String(50) | default Recruit |
rank progression |
| rank_points | Integer | default 0 | |
| aria_bonus_multiplier | Float | default 1.0 | 1.0-1.5 (migration f4a5b6c7d8e9) |
| aria_consciousness_level | Integer | default 1 | 1-5 |
| aria_relationship_score | Integer | default 25 | 0-100 |
| aria_total_interactions | Integer | default 0 | |
| aria_trust_score | Float | default 1.0 | 0.0–1.0; player-facing trust counter that ARIA's content-policy and abuse-detection layer reads. Drops on policy violations (jailbreak attempts, prompt injection, abusive content). Recovers slowly with sustained legitimate interaction. See ../OPERATIONS/aria.md, ../SYSTEMS/aria-dialogue.md. |
| aria_blocked_until | DateTime | nullable | Auto-block timer set when trust hits a floor; ARIA refuses interaction until expiry. Admin can clear via the trust-reset endpoint. |
| aria_violation_count | Integer | default 0 | Cumulative count of policy violations triggering trust drops. Resets on admin trust-reset; drives escalation thresholds (warn → soft-block → hard-block). |
| current_ship_id | UUID FK ships.id | nullable, SET NULL | |
| home_sector_id, current_sector_id | Integer | default 1 | integer sector numbers |
| is_docked | Boolean | default false | |
| current_port_id | UUID FK stations.id | nullable, SET NULL | station player is docked at (migration 5f5a988bdbb1); FK targets stations |
| is_landed | Boolean | default false | |
| current_planet_id | UUID FK planets.id | nullable, SET NULL | |
| team_id | UUID FK teams.id | nullable, SET NULL | |
| attack_drones, defense_drones, mines, genesis_devices | Integer | defaults 0 | aggregated counters |
| insurance | JSONB | nullable | |
| last_game_login | DateTime | nullable | distinct from User.last_login |
| turn_reset_at | DateTime | nullable | |
| settings | JSONB | default {} | UI / language / audio / accessibility / privacy / notifications / active bounties — full shape in jsonb-schema.md#playersettings |
| first_login | JSONB | default | quick flag, separate from FirstLoginSession trail |
| is_active | Boolean | default true | in-game deactivation |
| home_region_id, current_region_id | UUID FK regions.id | nullable | |
| is_galactic_citizen | Boolean | default false | unlocks cross-region travel |
| suspect_status | Boolean | default false | temporary "gray" flag (separate from persistent personal_reputation); fires from early-window Cargo Wreck salvage and similar antisocial-but-not-criminal actions. While true, fed-space immunity is suspended and name color renders gray/yellow. See ../FEATURES/gameplay/ranking.md#suspect-status-temporary-flag. |
| suspect_until | DateTime | nullable | timestamp when suspect_status auto-clears; max cumulative 4 hours from first triggering event. |
| suspect_team_snapshot | UUID[] | nullable | Per ADR-0061 S-V4 — frozen team-mate set captured at suspect-status acquisition. Team-mate exemptions during the grace window evaluate against this snapshot, not live team membership. Mid-grace team changes don't affect existing exemptions; the snapshot is taken once at first acquisition and not refreshed by extension events. Cleared when suspect_status clears. NULL when not flagged; empty array when the player had no team. |
| wanted_status | Boolean | default false | persistent "criminal" flag fired by piloting a ship reported stolen. While true, name renders red, fed-space immunity suspended, NPC patrols hunt, automatic bounty placed, daily personal-reputation drain. Clears when the pilot leaves the stolen ship. See ../FEATURES/gameplay/ranking.md#wanted-status, ../SYSTEMS/ship-registry.md. |
| wanted_until | DateTime | nullable | optional auto-clear timestamp; usually NULL (Wanted persists for the duration the player pilots the stolen ship). Set non-NULL only if a future design adds a fixed-duration Wanted state for non-stolen-ship triggers. Parallel field to suspect_until. |
| daily_faction_rep_consumed | Integer | default 0 | Per ADR-0056 N-V1. Sum of positive faction-rep deltas applied today across all factions. Compared against a single global daily pool (Launch: 100 rep / day) by apply_emergent_action before the rep delta lands. Reset to 0 by the throttle-rollover service per ADR-0053. The two cascade-redemption actions per ADR-0056 N-F2 bypass this counter. |
| daily_faction_rep_consumed_date | Date | nullable | The local date the daily_faction_rep_consumed counter applies to. Rollover service uses (consumed > 0 AND consumed_date < player_local_today) as the reset predicate. |
Relationships:
- user → User.
- ships → Ship (1:many, FK Ship.registered_owner_id per ADR-0008); current_ship → Ship (FK current_ship_id, post-update).
- team → Team (FK team_id); team_membership → TeamMember (1:1).
- faction_reputations → Reputation (1:many).
- planets (many-to-many via player_planets), stations (many-to-many via player_stations).
- discovered_sectors → Sector.
- genesis_devices → GenesisDevice (1:many — note: this shadows the integer counter genesis_devices defined on the same class earlier; the relationship wins at runtime).
- combat_logs_as_attacker, combat_logs_as_defender → CombatLog.
- created_warp_tunnels → WarpTunnel.
- market_transactions → resource.MarketTransaction (resource-enum table); enhanced_market_transactions → market_transaction.MarketTransaction (commodity-string table).
- first_login_sessions (1:many), first_login_state (1:1).
- regional_memberships → RegionalMembership; inter_regional_travels → InterRegionalTravel.
- AI: trading_profile, ai_recommendations, aria_memories, aria_market_intelligence, aria_exploration_map, aria_trading_observations, ai_assistant.
- commanded_fleets → Fleet; fleet_memberships → FleetMember.
- drones → Drone; drone_deployments → DroneDeployment.
Note: the genesis_devices name is overloaded (integer column + relationship). In practice the relationship overwrites the column attribute. The integer counter is still backed by the database column; access via Player.__table__.c.genesis_devices if you need it.
PlayerTradingProfile¶
Source: services/gameserver/src/models/ai_trading.py (PlayerTradingProfile)
Purpose: Per-player trading profile that aggregates ARIA's view of how the player trades — risk tolerance, commodity preferences, performance metrics. Distinct from ARIATradingPattern (Trade DNA per-pattern); this is the player-level summary.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | unique, CASCADE | 1:1 with Player |
| risk_tolerance | Float | default 0.5 | 0.0 = ultra-conservative; 1.0 = high-risk |
| preferred_commodities | JSONB | default [] |
array of commodity names ordered by player preference |
| avoided_sectors | JSONB | default [] |
array of sector compound IDs the player consistently avoids |
| ai_assistance_level | String(10) | default medium |
minimal / medium / full (see ../FEATURES/gameplay/aria-companion.md#player-controlled-assistance-level) |
| notification_preferences | JSONB | default {} |
per-event notification toggles (route_alerts, market_alerts, combat_alerts, genesis_alerts) |
| last_active_sector | UUID FK sectors.id | nullable | most recent sector the player traded from |
| average_profit_per_trade | Float | default 0.0 | rolling average across the trading history |
| total_trades_analyzed | Integer | default 0 | denominator for the rolling averages |
| created_at, updated_at | DateTime | defaults |
Relationships: player (1:1).
The model class exists; the schema fields above are the launch target. Currently unused beyond the relationship reference. (See "Schema status" at the top of this file.)
AIRecommendation¶
Source: services/gameserver/src/models/ai_trading.py (AIRecommendation)
Purpose: Persistent record of a recommendation ARIA surfaced to the player, with feedback-loop fields the trading pattern fitness loop reads.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | indexed, CASCADE | |
| recommendation_type | String(20) | not null | buy / sell / route / avoid |
| recommendation_data | JSONB | not null | service-private opaque shape (see ./jsonb-schema.md for redaction rules) |
| confidence_score | Float | default 0.5 | 0.0–1.0 |
| expected_profit | Numeric | nullable | predicted profit in credits |
| risk_assessment | String(10) | default medium |
low / medium / high |
| reasoning | Text | nullable | human-readable explanation surfaced in the UI |
| priority_level | Integer | default 3 | 1 (lowest) – 5 (highest); drives surface-priority in companion panel |
| created_at | DateTime | indexed, default utcnow | |
| expires_at | DateTime | nullable, indexed | recommendation auto-purges past this point |
| accepted | Boolean | nullable | NULL = pending; True = player accepted; False = player explicitly dismissed |
| accepted_at | DateTime | nullable | |
| outcome_profit | Numeric | nullable | actual profit captured post-hoc; feeds Trade DNA fitness |
| outcome_timestamp | DateTime | nullable | |
| feedback_score | Integer | nullable | 1–5 player rating |
| feedback_text | Text | nullable | optional player commentary |
Relationships: player (FK).
The model class exists; columns above are the launch target. Recommendation generation, acceptance tracking, outcome attribution, and the feedback loop into Trade DNA are all design-stage. Anti-gaming on the feedback loop is documented in ../OPERATIONS/aria.md#recommendation-anti-gaming. (See "Schema status" at the top of this file.)
FirstLoginSession + Friends¶
Source: services/gameserver/src/models/first_login.py
Purpose: Captures the AI-driven onboarding dialog where the player tries to claim a starter ship from a guard; logs negotiation outcomes and drives starting state.
FirstLoginSession¶
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | not null, CASCADE | |
| started_at | DateTime | server default now | |
| completed_at | DateTime | nullable | |
| ai_service_used | Boolean | default false | |
| fallback_to_rules | Boolean | default false | |
| guard_name, guard_title, guard_trait, guard_description | String | nullable | personality bundle (migration ec92f8afd44a) |
| guard_base_suspicion | Float | nullable | |
| ship_claimed | Enum ship_choice |
nullable | what player tried to claim |
| extracted_player_name | String | nullable | |
| negotiation_skill | Enum negotiation_skill_level |
nullable | WEAK/AVERAGE/STRONG |
| final_persuasion_score | Float | nullable | |
| outcome | Enum dialogue_outcome |
nullable | SUCCESS/PARTIAL_SUCCESS/FAILURE |
| awarded_ship | Enum awarded_ship_type |
nullable | |
| starting_credits | Integer | nullable | |
| negotiation_bonus_flag, notoriety_penalty | Boolean | nullable | |
| client_info, performance_metrics | JSONB | nullable |
Relationships: player, dialogue_exchanges (1:many cascade), ship_options (1:1 cascade).
DialogueExchange¶
Per-turn record of the negotiation. Key columns: session_id (FK), sequence_number, npc_prompt, player_response, topic, persuasiveness, confidence, consistency, key_extracted_info JSONB, detected_contradictions ARRAY(String). AI logging fields (migration 6acc65ee7a72): ai_provider, ai_system_prompt, ai_user_prompt, ai_raw_response, believability, current_suspicion, response_time_ms, estimated_cost_usd, tokens_used.
ShipPresentationOptions¶
Per-session ship offering. Columns: session_id FK, available_ships ARRAY(String), escape_pod_present (default true), rarity_roll (0-100), special_event_active, seed_value.
ShipRarityConfig¶
Static config: ship_type (unique), rarity_tier (1-5), spawn_chance (0-100), base_credits, and three persuasion thresholds (weak_threshold, average_threshold, strong_threshold). Several migrations rebalance these (2e78250f47bc, 6b1d95a38c98, c5e32c313020).
PlayerFirstLoginState¶
Tracks completion across sessions. Columns: player_id (unique FK), has_completed_first_login, current_session_id (FK), attempts, last_attempt_at, completion bools (claimed_ship, answered_questions, received_resources, tutorial_started), and history arrays previous_ship_claims, previous_dialogue_strategies.
Team¶
Source: services/gameserver/src/models/team.py
Purpose: Player alliance with shared treasury, sector claims, and reputation aggregation policy.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| name | String(80) | unique, not null | |
| description | Text | nullable | |
| leader_id | UUID | nullable | not a FK constraint; resolved via Player.team_id and the leader check |
| reputation_calculation_method | String(20) | default AVERAGE |
AVERAGE/LOWEST/LEADER |
| tag | String(10) | nullable | display tag |
| logo | String | nullable | URL |
| is_public | Boolean | default true | |
| max_members | Integer | default 4 | |
| sector_claims | ARRAY(Integer) | default [] | claimed sector numbers |
| home_sector_id | Integer | nullable | |
| recruitment_status | String(20) | default OPEN |
OPEN/INVITE_ONLY/CLOSED |
| treasury_credits, treasury_fuel, treasury_organics, treasury_equipment, treasury_technology, treasury_luxury_items, treasury_precious_metals, treasury_raw_materials, treasury_plasma, treasury_bio_samples, treasury_dark_matter, treasury_quantum_crystals | Integer | default 0 | shared treasury per resource |
| total_credits, total_planets | Integer | default 0 | aggregates |
| combat_rating, trade_rating | Float | default 0.0 | |
| join_requirements, member_roles, resource_sharing, invitation_codes | JSONB | defaults |
Relationships:
- members → Player (1:many via Player.team_id).
- team_members → TeamMember (1:many cascade) — adds roles/permissions on top of plain membership.
- reputation → TeamReputation (1:1 cascade).
- controlled_sectors → Sector.
- drones, fleets, messages.
TeamMember¶
Per-member roles/permissions. Columns: team_id FK, player_id FK, role (LEADER/OFFICER/MEMBER/RECRUIT), joined_at, permissions JSONB, individual booleans can_invite, can_kick, can_manage_treasury, can_manage_alliances, last_active, contribution_credits JSONB.
Reputation¶
Source: services/gameserver/src/models/reputation.py
Purpose: Per-(player, faction) reputation row with a 17-tier enum and effects flags.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | not null, CASCADE | |
| faction_id | UUID FK factions.id | not null, CASCADE | |
| current_value | Integer | default 0 | numeric score |
| current_level | Enum reputation_level |
default NEUTRAL | 17 tiers from PUBLIC_ENEMY (-8) to EXALTED (+8) |
| title | String(50) | default Neutral |
display |
| decay_paused | Boolean | default false | |
| history | JSONB | default [] | |
| trade_modifier | Float | default 0 | |
| port_access_level | Integer | default 0 | |
| combat_response | String(50) | default neutral |
one of friendly (Honored+), neutral (default), cautious (mid-negative), hostile (Hostile/Hated band), attack_on_sight (Public Enemy / Criminal). Drives NPC patrol behavior toward the player at faction-flagged stations and in faction-controlled sectors. |
| is_locked | Boolean | default false | admin lock |
| lock_reason, lock_expires, special_status | mixed | nullable |
Relationships: player, faction.
Indexes (per ADR-0051 SK32):
- (player_id, faction_id) UNIQUE composite — primary access pattern: "what's my rep with faction X."
- (faction_id, current_value DESC) — leaderboard queries ("top reputation with faction X").
- (player_id) — full scan over a player's faction rows.
Canonical query patterns documented to keep new query authors aware. New patterns require an index review.
TeamReputation¶
Aggregated team reputation. Columns: team_id (unique FK), calculation_method (AVERAGE/LOWEST/LEADER), faction_reputation JSONB, history JSONB, last_recalculated, next_recalculation, pending_notifications JSONB.
PlayerCentralBankAccount¶
Per ADR-0050. Region-independent depository at the Central Nexus, operated by the Galactic Concord. One account per player. Stores credits and commodities indefinitely. Receives planet-safe transfers and station credit-compensation fallbacks during region-termination cascades; players withdraw at any Starport Prime dock in the Central Nexus. Cross-link surfaces (per ADR-0065 R-O2): the cascade-deposit flow lives in ../SYSTEMS/region-lifecycle.md; the subscription-tier-driven Bank-access semantics live in ../OPERATIONS/monetization.md.
| name | type | constraints | notes |
|---|---|---|---|
| player_id | UUID FK players.id | PK, not null | One account per player |
| credits | Integer | not null, default 0 | Liquid; transfer to wallet free at any Starport Prime |
| commodities | JSONB | not null, default {} |
{ore: 5000, organics: 2000, ...} per the canonical commodity enum |
| ledger | JSONB | not null, default [] |
Append-only entries describing deposits/withdrawals for player audit. Each entry: {timestamp, type, amount, source, notes, access_override}. access_override BOOLEAN (per ADR-0054 X-I3) flags cascade-deposit entries that grant Bank withdrawal access regardless of current GC subscription status. |
| created_at, updated_at | DateTime | server defaults |
Operations (per ../SYSTEMS/region-lifecycle.md Bank service):
- Deposit — only by the cascade orchestrator (planet-safe transfer, station credit-compensation fallback). No normal player-side deposit; the Bank is a one-way recipient from cascade events.
- Withdraw credits — instant, free, at any Starport Prime dock.
- Withdraw commodities — 1 turn per 100 units (rounded up); to player's ship cargo; at any Starport Prime dock; subject to ship cargo space.
The ledger entries persist for the player's lifetime as audit history. Common entry types: cascade_safe_transfer (with 20% loss flag if Path A), cascade_station_compensation (50%-acquisition fallback), withdraw_credits, withdraw_commodity.
Bank access during GC lapse (per ADR-0054 X-I3): Bank operations normally require an active Galactic Citizen subscription. If a player's GC subscription has lapsed but their account holds credits/commodities deposited via cascade compensation (i.e., the ledger has at least one prior entry with access_override = true), withdrawals remain enabled FIFO until those access-override balances are drawn down. Logic: deposits made by the cascade orchestrator carry access_override = true; withdrawals consume the oldest access-override entries first; once the cumulative access-override balance reaches zero, normal GC-required gating resumes. This prevents the regress where a region terminates, the player's assets land in the Bank, then GC lapses and traps the assets.
TakeoverIntent¶
Per ADR-0050. Records a player's intent to take over a Suspended/Grace region while the PayPal payment flow runs. Cleaned up by a periodic sweep on expiry.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| region_id | UUID FK regions.id | not null | The region being taken over |
| caller_user_id | UUID FK users.id | not null | The player making the offer |
| approval_url | String | not null | PayPal flow approval URL returned to client |
| status | Enum (pending, won, lost, transferred, failed, expired) |
default pending |
Per ADR-0058 A-F3. State machine: pending → won (claim accepted, region transfer follows), pending → lost (claim rejected by serializer; escrow refunded in same tx), won → transferred (transfer succeeded), won → failed (transfer failed; escrow refunded; region stays with old owner), pending → expired (PayPal flow window timed out). Every transition is atomic. |
| created_at | DateTime | server default | |
| expires_at | DateTime | not null | created_at + 1 hour (the PayPal flow window) |
| completed_at | DateTime | nullable | Set when commit_takeover() runs |
Indexes: (region_id, status) for "pending intents on this region"; (expires_at) for the periodic-sweep cleanup; (region_id) WHERE status = 'pending' for the SELECT FOR UPDATE serializer per ADR-0058 A-F3.
Concurrent-claim serialization (per ADR-0058 A-F3): when multiple takeover claims race on the same region, the takeover handler opens a transaction and runs SELECT * FROM regions WHERE id = :region_id FOR UPDATE before evaluating the claim. First commit wins (status = 'won'); concurrent claims see the won state inside their own lock acquisition and reject (status = 'lost') with the offer escrow refunded inside the same transaction. No temporal window where money is held without a corresponding ownership state.
PlayerWarpKnowledge¶
Per ADR-0045 and ADR-0065 R-I3. Per-player warp discovery state — the source of truth for "what does this player know about the warp graph." Map-visibility queries read against it; ARIA reads it for hint generation per ADR-0045.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| player_id | UUID FK players.id | not null, CASCADE | composite PK part 1 |
| warp_id | UUID | not null | composite PK part 2; references either sector_warps or WarpTunnel (polymorphic per warp_type). |
| warp_type | Enum (sector_warp, warp_tunnel) |
not null | Disambiguates the polymorphic FK target. |
| discovered_at | DateTime | not null | First time this player became aware of the warp (any source). |
| discovery_source | Enum (personal_visit, warp_jumper_scan, corp_share, aria_hint) |
not null | How the discovery happened. corp_share propagation may upgrade to personal_visit later (record updates, not duplicates). |
| corp_shared_at | DateTime | nullable | Set when a corp-mate's discovery propagates to this player. Distinct from discovered_at so the system can distinguish first-hand from second-hand knowledge. |
Composite PK: (player_id, warp_id).
Indexes:
- (player_id) — "what does this player know" (the dominant query).
- (warp_id) — "who knows about this warp" (rare; for ops/audit queries).
The Nexus warp visibility filter per ADR-0064 R-V3 reads this table — free-tier players who have a corp-mate's discovered_at set on the Nexus warp are still excluded from the visible-on-map set, because the filter intersects this table with the player's subscription tier.
Message¶
Source: services/gameserver/src/models/message.py
Purpose: Player-to-player and team messaging with threading and moderation.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| sender_id | UUID FK players.id | not null, indexed | |
| recipient_id | UUID FK players.id | nullable, indexed | null for team messages |
| team_id | UUID FK teams.id | nullable, indexed | null for direct messages |
| subject | String(255) | nullable | |
| content | Text | not null | |
| sent_at | DateTime | default utcnow, indexed | |
| read_at | DateTime | nullable | |
| deleted_by_sender, deleted_by_recipient | Boolean | defaults false | soft delete per side |
| thread_id | UUID | indexed | conversation grouping |
| reply_to_id | UUID FK messages.id | nullable | direct reply |
| message_type | String(20) | default player |
player/team/system |
| priority | String(10) | default normal |
low/normal/high/urgent |
| flagged | Boolean | default false | |
| flagged_reason | String(255) | nullable | |
| moderated_at | DateTime | nullable | |
| moderated_by | UUID FK users.id | nullable | admin user |
Relationships: sender, recipient, team, reply_to (self-ref), moderator (User).
Composite indexes: (recipient_id, read_at), (team_id, sent_at), (thread_id, sent_at).
Auth side-tables (referenced)¶
PlayerCredentials, AdminCredentials (see ./admin.md), OAuthAccount, RefreshToken, MFASecret, MFAAttempt all hang off User 1:1 or 1:many. They are mechanical auth plumbing — no game logic — and not expanded here.