DM как полный демиург

DM может что угодно. Объясняет, почему права DM не ограничиваются guardrail'ами, как обеспечивается safety через следы, и что именно DM не может технически.

DM — это Бог кампании. Вставить NPC из ниоткуда, откатить решение игрока, поменять цены в лавке задним числом, удалить персонажа — всё это рабочие операции. Ограничивать DM правилами системы значит создавать систему, которая мешает вести игру.

Safety обеспечивается не через запреты, а через аудит и следы.


Как устроены права DM сейчас

Все server actions в app/actions/*.ts используют createAdminClient() (service role) для записи — это bypass RLS. RLS применяется к прямым клиентским чтениям из lib/queries/*, но пути записи идут как admin.

Поэтому каждый server action начинается с auth check:

  • resolveAuth(campaignId) — возвращает { role }. Если role не dm или owner — action возвращает ошибку без записи.
  • getMembership(campaignId) — базовая проверка «является ли участником».
  • canEditNode(nodeId, campaignId, userId, role) — per-node гейтинг, зеркалит RLS-политику can_edit_node.

RLS на таблицах — safety net на случай, если route handler забыл свой гейт и использует user client. Это второй рубеж, а не первый.

Что DM может

Практически всё, что касается контента кампании:

  • Создать, отредактировать, удалить любую ноду — NPC, локацию, предмет, сессию, петлю. Через can_edit_node, который для DM всегда true.
  • Записать транзакцию за любого PC — деньги, предметы, переводы.
  • Подтвердить или отклонить заявки игроков (statusapproved / rejected).
  • Управлять составом кампании — пригласить, повысить, удалить участника (/c/[slug]/members).
  • Редактировать PC любого игрока — через is_dm_or_owner(campaign_id).

Что DM не может

Четыре жёстких ограничения на уровне системы:

  1. Уронить базу. DROP TABLE, TRUNCATE, DDL — недоступны. Суперuser — только у DevOps, не у приложения.
  2. Читать или писать чужие кампании. campaign_id проверяется на каждом уровне; is_member / is_dm_or_owner — SECURITY DEFINER функции.
  3. Писать в auth.users. Auth — зона Supabase; приложение работает только с user_profiles и campaign_members.
  4. Удалить собственную кампанию через стандартный UI — мягкая защита (нет кнопки), а не техническое ограничение. Каскадное удаление в схеме есть (ON DELETE CASCADE), но через admin action, которой не существует.

Safety layer: следы вместо запретов

В проде сейчас audit log и soft delete отсутствуют. DM правит напрямую; hard DELETE на чувствительных таблицах возможен.

Целевая архитектура:

  • event.deleted_at — nullable timestamp вместо DELETE. Удалённое остаётся в БД с меткой, фильтруется на уровне RLS/queries.
  • Версионирование контентаitem_version, monster_version: правка создаёт новую версию, старые остаются; события ссылаются на конкретную версию.
  • dm_audit_log — каждая DM-операция пишется с action_type, target_id, before_state, after_state, timestamp. Читается только owner.

roadmap/audit-log-and-safety.md

Разделение DM-операций в целевой модели

В HANDOFF DM-операции — именованные типы событий:

Event typeСмысл
dm_overwriteЗамена значения (позиция NPC, погода, данные локации)
dm_injectВставка сущности (новый NPC, предмет, событие)
dm_correctionКоррекция прошлого события с обоснованием

Сейчас DM пишет напрямую в таблицы через server actions без этих типов — универсального events лога ещё нет.


Принцип: полная власть + полный аудит. DM — демиург, но не анонимный.

roles-and-clients.md — чем DM отличается от player и owner в системе ролей. → tool-first.md — почему инструмент работает на DM, а не ограничивает его.