ADR-009: Админы и клиенты — раздельные популяции
Status: Accepted · Date: 2026-07-04
Контекст
Section titled “Контекст”Предложение: единая сущность User с ролью (user | admin) и таблицей identities (type: password | oauth | ...), один пул аккаунтов для операторов панели и клиентов VPN.
Решение
Section titled “Решение”Две независимые популяции: admins (пароль + OAuth, роли owner | admin | viewer) и clients (magic link, без ролей). Разделение — на уровне таблиц и JWT-scope (admin | client), проверяемого middleware до бизнес-логики. См. Аутентификацию.
Отклонённые альтернативы
Section titled “Отклонённые альтернативы”Единый User с ролью. Клиенты — не «пользователи панели с меньшими правами», а другая популяция с другим жизненным циклом: создаются биллингом, а не приглашением; без пароля by design; их предметная область — подписки, а не операции. Слияние даёт: поверхность эскалации привилегий в один UPDATE role (при раздельных таблицах клиент структурно не может стать админом); невозможность одному email быть и оператором, и покупателем (легитимный случай); вечный WHERE role = ... в каждом запросе к «пользователям».
Таблица identities с type = password. Пароль строго 1:1 с аккаунтом — полиморфная строка вместо колонки добавляет join и nullable-поля ради гибкости без сценария. OAuth-identity остаётся отдельной таблицей (их может быть несколько на аккаунт) — «один аккаунт, много способов входа» уже выполнено без обобщения.
Последствия
Section titled “Последствия”- (+) Компрометация клиентского флоу (magic link) не граничит с админскими правами ни в одной таблице.
- (+) Каждая популяция эволюционирует независимо (2FA для админов не трогает клиентов).
- (−) Общий код сессий параметризован
subject_type— единственная точка, где популяции встречаются.