Database
Current Database Schema
The ASCII diagram below reflects the current SQLite schema in data/mud.db.
+----------------------------------+
| users |
+----------------------------------+
| PK id INTEGER |
| NN username TEXT | UNIQUE
| NN password_hash TEXT |
| email_hash TEXT | UNIQUE
| NN role TEXT | DEFAULT 'player'
| NN is_active INTEGER | DEFAULT 1, CHECK IN (0, 1)
| NN is_guest INTEGER | DEFAULT 0, CHECK IN (0, 1)
| guest_expires_at TIMESTAMP |
| NN account_origin TEXT | DEFAULT 'legacy'
| created_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
| last_login TIMESTAMP |
| tombstoned_at TIMESTAMP |
+----------------------------------+
+----------------------------------+
| characters |
+----------------------------------+
| PK id INTEGER |
| user_id INTEGER | FK -> users.id (ON DELETE SET NULL)
| NN name TEXT |
| NN world_id TEXT | PART OF UNIQUE(world_id, name)
| NN inventory TEXT | DEFAULT '[]'
| NN is_guest_created INTEGER | DEFAULT 0, CHECK IN (0, 1)
| created_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
| updated_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
| base_state_json TEXT |
| current_state_json TEXT |
| state_seed INTEGER | DEFAULT 0, CHECK >= 0
| state_version TEXT |
| state_updated_at TIMESTAMP |
+----------------------------------+
+----------------------------------+
| character_locations |
+----------------------------------+
| PK character_id INTEGER | FK -> characters.id (ON DELETE CASCADE)
| NN world_id TEXT |
| NN room_id TEXT |
| updated_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| sessions |
+----------------------------------+
| PK id INTEGER |
| NN user_id INTEGER | FK -> users.id (ON DELETE CASCADE)
| character_id INTEGER | FK -> characters.id (ON DELETE SET NULL)
| world_id TEXT |
| NN session_id TEXT | UNIQUE
| created_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
| last_activity TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
| expires_at TIMESTAMP |
| client_type TEXT | DEFAULT 'unknown'
+----------------------------------+
+----------------------------------+
| chat_messages |
+----------------------------------+
| PK id INTEGER |
| character_id INTEGER | FK -> characters.id (ON DELETE SET NULL)
| user_id INTEGER | FK -> users.id (ON DELETE SET NULL)
| NN message TEXT |
| NN world_id TEXT |
| NN room TEXT |
| recipient_character_id INTEGER | FK -> characters.id (ON DELETE SET NULL)
| timestamp TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| worlds |
+----------------------------------+
| PK id TEXT |
| NN name TEXT |
| description TEXT |
| NN is_active INTEGER | DEFAULT 1, CHECK IN (0, 1)
| NN config_json TEXT | DEFAULT '{}'
| created_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| world_permissions |
+----------------------------------+
| PK user_id INTEGER | FK -> users.id (ON DELETE CASCADE)
| PK world_id TEXT | FK -> worlds.id (ON DELETE CASCADE)
| NN can_access INTEGER | DEFAULT 1, CHECK IN (0, 1)
| created_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| axis |
+----------------------------------+
| PK id INTEGER |
| NN world_id TEXT |
| NN name TEXT |
| description TEXT |
| ordering_json TEXT |
+----------------------------------+
+----------------------------------+
| axis_value |
+----------------------------------+
| PK id INTEGER |
| NN axis_id INTEGER | FK -> axis.id (ON DELETE CASCADE)
| NN value TEXT |
| min_score REAL |
| max_score REAL |
| ordinal INTEGER |
+----------------------------------+
+----------------------------------+
| event_type |
+----------------------------------+
| PK id INTEGER |
| NN world_id TEXT |
| NN name TEXT |
| description TEXT |
+----------------------------------+
+----------------------------------+
| character_axis_score |
+----------------------------------+
| PK character_id INTEGER | FK -> characters.id (ON DELETE CASCADE)
| PK axis_id INTEGER | FK -> axis.id (ON DELETE CASCADE)
| NN world_id TEXT |
| NN axis_score REAL |
| updated_at TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| event |
+----------------------------------+
| PK id INTEGER |
| NN world_id TEXT |
| NN event_type_id INTEGER | FK -> event_type.id
| timestamp TIMESTAMP | DEFAULT CURRENT_TIMESTAMP
+----------------------------------+
+----------------------------------+
| event_entity_axis_delta |
+----------------------------------+
| PK id INTEGER |
| NN event_id INTEGER | FK -> event.id (ON DELETE CASCADE)
| NN character_id INTEGER | FK -> characters.id (ON DELETE CASCADE)
| NN axis_id INTEGER | FK -> axis.id (ON DELETE CASCADE)
| NN old_score REAL |
| NN new_score REAL |
| NN delta REAL |
+----------------------------------+
+----------------------------------+
| event_metadata |
+----------------------------------+
| PK id INTEGER |
| NN event_id INTEGER | FK -> event.id (ON DELETE CASCADE)
| NN key TEXT |
| NN value TEXT |
+----------------------------------+
+----------------------------------+
| policy_item |
+----------------------------------+
| PK policy_id TEXT |
| NN policy_type TEXT |
| NN namespace TEXT |
| NN policy_key TEXT |
| NN created_at TEXT | DEFAULT CURRENT_TIMESTAMP
| UNI(policy_type, namespace, policy_key)
+----------------------------------+
+----------------------------------+
| policy_variant |
+----------------------------------+
| PK id INTEGER |
| NN policy_id TEXT | FK -> policy_item.policy_id (ON DELETE CASCADE)
| NN variant TEXT |
| NN schema_version TEXT |
| NN policy_version INTEGER |
| NN status TEXT | CHECK IN (draft, candidate, active, archived)
| NN content_json TEXT |
| NN content_hash TEXT |
| NN updated_at TEXT |
| NN updated_by TEXT |
| UNI(policy_id, variant)
+----------------------------------+
+----------------------------------+
| policy_activation |
+----------------------------------+
| PK world_id TEXT | FK -> worlds.id (ON DELETE CASCADE)
| PK client_profile TEXT | DEFAULT ''
| PK policy_id TEXT | FK -> policy_item.policy_id (ON DELETE CASCADE)
| NN variant TEXT | FK -> policy_variant(policy_id, variant)
| NN activated_at TEXT |
| NN activated_by TEXT |
| rollback_of_activation_id INTEGER |
+----------------------------------+
+----------------------------------+
| policy_validation_run |
+----------------------------------+
| PK id INTEGER |
| NN policy_id TEXT |
| NN variant TEXT |
| NN is_valid INTEGER | CHECK IN (0, 1)
| NN errors_json TEXT | DEFAULT '[]'
| NN validated_at TEXT |
| NN validated_by TEXT |
+----------------------------------+
+----------------------------------+
| policy_audit_event |
+----------------------------------+
| PK id INTEGER |
| NN event_type TEXT |
| world_id TEXT |
| NN client_profile TEXT | DEFAULT ''
| policy_id TEXT |
| variant TEXT |
| NN actor TEXT |
| NN event_payload_json TEXT | DEFAULT '{}'
| NN created_at TEXT |
+----------------------------------+
+----------------------------------+
| policy_publish_run |
+----------------------------------+
| PK id INTEGER |
| NN world_id TEXT | FK -> worlds.id (ON DELETE CASCADE)
| NN client_profile TEXT | DEFAULT ''
| NN actor TEXT |
| NN manifest_json TEXT |
| NN created_at TEXT |
+----------------------------------+
Notes
users.email_hashstores a hashed email address (no plaintext email). The column is nullable during development but intended to be required later; the unique index keeps that migration path open.users.is_guest+users.guest_expires_atmodel temporary accounts that are auto-purged; the user row is deleted and related characters are unlinked (user_idset to NULL) rather than deleted.characters.nameis a plain TEXT field, so names with spaces (e.g., first + last) are supported; uniqueness is enforced per world byUNIQUE(world_id, name).Session integrity is enforced by SQLite triggers: account-only sessions must keep both
character_idandworld_idNULL; in-world sessions must set both and match character ownership/world.Axis state is tracked in normalized tables (
axis,axis_value,character_axis_score) with an event ledger (event*tables).Canonical policy control-plane state lives in
policy_*tables. Runtime policy reads use effectivepolicy_activationpointers rather than world package files.world_permissionsstores invite-style access grants. Open-world access is policy-driven from config and may not require a row in this table.Hot-path indexes are intentionally maintained for world-scoped session activity, character ownership queries, and room chat history lookups.
Runtime code should import DB operations from
mud_server.db.facade;mud_server.db.databaseis a compatibility re-export surface.