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_hash stores 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_at model temporary accounts that are auto-purged; the user row is deleted and related characters are unlinked (user_id set to NULL) rather than deleted.

  • characters.name is a plain TEXT field, so names with spaces (e.g., first + last) are supported; uniqueness is enforced per world by UNIQUE(world_id, name).

  • Session integrity is enforced by SQLite triggers: account-only sessions must keep both character_id and world_id NULL; 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 effective policy_activation pointers rather than world package files.

  • world_permissions stores 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.database is a compatibility re-export surface.