Этап 0: документация и макеты

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreypr207andClaude Opus 5.5 committed 2026-10-21 03:30:38 +02:00
commit 3400f58a5a
75 files changed
+14283

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
# D-001. Одно Next.js-приложение на все домены
Статус: принято, 20.10.2026.
## Контекст
Два бренда, у каждого сайт, кабинет и MiniApp — 6 доменов плюс админка. Бизнес-логика общая, дизайны разные.
## Решение
1. Одно Next.js-приложение, Payload установлен в него как библиотека.
2. Proxy Next.js определяет по Host сайт и тип фронтенда и переписывает запрос во внутренний маршрут.
3. Ядро (биллинг, ключи, вход, рефералы, почта) и UI-кит общие, страницы у каждого бренда свои.
## Рассмотренные варианты
1. Отдельные приложения на каждый фронтенд и отдельный Payload как API — больше деплоев, логика дублируется или ходит по HTTP.
2. Один набор страниц с переключением цветов — не подходит: данные в дизайнах представлены по-разному.
## Последствия
1. Один процесс на окружение, один деплой.
2. Кабинет и сайт читают данные через Local API Payload, без HTTP.
3. Нужна дисциплина: бренд-зависимый код — только в `src/brands/`.
+21
View File
@@ -0,0 +1,21 @@
# D-002. Payload 3, переход на 4 после стабильного релиза
Статус: принято, 20.10.2026.
## Контекст
На 20.10.2026 стабильная версия — Payload 3.91.0. Payload 4 в бете (4.0.0-beta.2 от 16.10.2026, требует Node ≥ 24.15 и Next.js ≥ 16.4). У Payload есть официальный codemod для перехода с 3 на 4.
## Решение
1. Стартуем на последней 3.x и Next.js 16.4.x.
2. Код пишем совместимо с 4: в Local API всегда явно `overrideAccess`, rich text — только Lexical, без устаревших API.
3. Переходим на 4 после стабильного релиза. Если он выйдет до старта этапа 1a — начинаем сразу на 4.
## Рассмотренные варианты
Начать на бете 4 — нет переезда потом, но риск ошибок и меняющихся API в бете, которой неделя.
## Последствия
Один плановый переезд, пока данных в prod мало или нет.
+22
View File
@@ -0,0 +1,22 @@
# D-003. Без Docker: всё на хосте, процессы под systemd
Статус: принято, 20.10.2026.
## Контекст
На сервере уже работают PostgreSQL 18, nginx и Redis. Пользователь хочет, чтобы dev и prod запускались одинаково и без Docker.
## Решение
1. PostgreSQL и nginx — на хосте.
2. Приложение — обычный процесс Node: dev — `pnpm dev`, prod — `pnpm build` и `pnpm start`. Оба — systemd user-юниты: dev у пользователя vx_dev, prod у vx_prod ([D-004](D-004-environments.md)).
3. Prod выкладывается в отдельную папку релиза и переключается симлинком `current`. Откат — переключить симлинк обратно.
## Рассмотренные варианты
Docker-образ для prod — воспроизводимая сборка и изоляция, но лишний слой. Тот же откат даёт схема с папками релизов.
## Последствия
1. Сборка prod идёт на этом же сервере (нагрузка 2–4 ГБ памяти на время сборки).
2. Node и pnpm общие для dev и prod: обновляются вместе.
+41
View File
@@ -0,0 +1,41 @@
# D-004. Dev и prod на одном сервере под разными пользователями
Статус: принято, 21.10.2026.
## Контекст
Dev и prod живут на одном сервере без Docker. Редактор и сессии Claude работают в dev и не должны иметь доступа к prod.
## Решение
| | dev | prod |
|---|---|---|
| Пользователь ОС | vx_dev | vx_prod |
| Папка | `/home/vx_dev/veilix_dev` | `/home/vx_prod/veilix` |
| База и роль | `veilix_dev`, роль `vx_dev` | `veilix_prod`, роль `vx_prod` |
| Порт | 3100 | 3000 |
| Домены | `dev.veilix.app`, `dev.my.veilix.app`, `dev.tg.veilix.app`, так же для veilixvpn.com; `dev.plc.veilix.cc` | основные; `plc.veilix.cc` |
1. vx — личный пользователь администратора, к проекту отношения не имеет.
2. Вся разработка — под vx_dev: VSCode (Remote-SSH), Claude Code и другие ИИ-агенты запускаются только под этим пользователем.
3. Под vx_prod — только prod:
a. редактора и ИИ-агентов нет, VSCode Remote под vx_prod не подключаем: при подключении VSCode ставит на сервер свою серверную часть и может подтянуть туда расширения, в том числе ИИ-агентов;
b. своего SSH-входа нет, под vx_prod заходят из root (`sudo -iu vx_prod`).
4. Оба окружения запускаются одинаково — systemd user-юнитами, каждый у своего пользователя.
5. Домашние папки — `750`, владелец — сам пользователь: секреты, файлы и логи prod сессиям Claude недоступны.
6. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет; чужие базы закрыты `REVOKE CONNECT … FROM PUBLIC`.
7. Код попадает в prod только через Gitea: из dev публикуется тег, vx_prod забирает его ключом только на чтение и собирает релиз.
8. Выкладку запускает пользователь под vx_prod. По желанию — sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки (Q-18).
9. Домены хранятся в базе, поэтому у dev просто свои записи в своей базе.
## Рассмотренные варианты
1. Один пользователь для обоих окружений — проще, но сессии Claude и любой процесс dev технически достают до секретов и базы prod.
2. Prod под личным пользователем vx — под ним работают редактор и ИИ-агенты, защита держалась бы только на дисциплине.
3. Docker-контейнеры для изоляции — пользователь не хочет Docker.
## Последствия
1. Ошибки prod сессии Claude не видят напрямую: логи присылает пользователь.
2. Отдельные deny-правила Claude Code для prod не нужны.
3. Переезд разработки на vx_dev — задача R-00 ([09-infrastructure.md](../09-infrastructure.md), раздел 8).
+18
View File
@@ -0,0 +1,18 @@
# D-005. Сайты — тенанты, домены в базе, маршрутизация по Host
Статус: принято, 20.10.2026.
## Решение
1. Официальный плагин `@payloadcms/plugin-multi-tenant`: тенант — Сайт, поле `site` на коллекциях, привязанных к сайту, переключатель сайта в админке.
2. Коллекция «Домены»: хост, сайт, тип фронтенда, языки, валюты.
3. Proxy Next.js находит домен по Host (с кэшем в памяти) и переписывает запрос во внутренний маршрут.
4. Общие справочники без привязки к сайту: страны, теги, валюты, платёжные системы. Платёжная система привязана к валютам, а сайт выводится через валюты его доменов ([D-010](D-010-currencies.md)).
## Рассмотренные варианты
Простое поле «сайт» без плагина — придётся самим писать фильтры и доступ в админке.
## Последствия
Определение сайта по домену плагин не делает — это наш код в proxy.
+20
View File
@@ -0,0 +1,20 @@
# D-006. Переводы: поля Payload и next-intl; 5 языков
Статус: принято, 20.10.2026.
## Решение
1. Языки: `en` (по умолчанию, без префикса в URL), `ru`, `de`, `fr`, `es`. Позже — `pt-br` и `ja`.
2. Контент — локализованные поля Payload (`localized: true`), откат на английский.
3. Интерфейс — next-intl, файлы `messages/*.json` в репозитории.
4. Язык всегда в URL (кроме английского). Список языков — в конфиге, у домена — включённые языки. Отдельной сущности «Языки» нет.
5. В админке Payload `filterAvailableLocales` скрывает языки, выключенные для сайта.
## Рассмотренные варианты
1. Языки как записи в базе — Payload задаёт локали в конфиге, сущность дублировала бы конфиг.
2. Интерфейсные строки тоже в Payload — нет версионирования и типизации, лишние запросы.
## Последствия
Новый язык — правка конфига, переводы и деплой.
@@ -0,0 +1,17 @@
# D-007. Определение языка; MaxMind через nginx
Статус: принято, 20.10.2026.
## Решение
1. Порядок: язык в URL → сохранённый выбор (cookie, профиль) → `Accept-Language` → страна по MaxMind → `en`.
2. Перенаправление — только при первом заходе без префикса и только людей, не поисковых ботов. Плюс `hreflang` и `x-default`.
3. Страну определяет nginx (модуль geoip2, база GeoLite2-Country) и передаёт заголовком `X-Geo-Country`. Приложение само читает `.mmdb` только там, где нужна страна произвольного IP.
## Рассмотренные варианты
Язык только по стране — страна не равна языку, а Google не рекомендует перенаправлять по IP.
## Последствия
База MaxMind обновляется `geoipupdate` (root).
+16
View File
@@ -0,0 +1,16 @@
# D-008. UUID и случайные публичные коды
Статус: принято, 20.10.2026.
## Решение
1. `idType: 'uuid'` в адаптере Postgres Payload: все id в базе, REST и URL админки — UUID. Автоинкремента нет.
2. Номера для людей — короткие случайные коды: заказы `VX-7K3M9Q`, ключи `K-…`, платежи, реферальные коды.
## Рассмотренные варианты
Автоинкремент плюс отдельное поле UUID для внешнего мира — нужно следить, чтобы внутренний id нигде не утёк. Штатная настройка Payload решает задачу без кастома.
## Последствия
Уникальность случайных кодов обеспечивается уникальным индексом и повтором генерации при коллизии.
+24
View File
@@ -0,0 +1,24 @@
# D-009. Заказы, платежи, движения по счёту; всё через баланс
Статус: принято, 20.10.2026.
## Контекст
У пользователя есть счёт. Заказ можно оплатить картой, с баланса или частично. Нужна ручная оплата из админки, которая запускает те же действия, что и настоящая.
## Решение
1. Три сущности: Заказ (что покупают), Платёж (деньги извне), Движение по счёту (неизменяемые записи «+» и «−»).
2. Любой успешный платёж зачисляется на баланс, любой заказ списывается с баланса.
3. Одна точка проведения `processPayment`, идемпотентная.
4. Ручная оплата — платёжная система `manual`. Платёж проводится, когда получает статус `succeeded`. Галочки «провести как реальную» нет.
5. Корректировка баланса — отдельная операция: только движение, без цепочки действий.
## Рассмотренные варианты
1. Одна сущность «транзакция» — смешивает коммерческий документ, деньги провайдера и баланс; сложно с частичной оплатой и повторами вебхуков.
2. Галочка «провести как реальную оплату» — не нужна: проведение запускает смена статуса.
## Последствия
Правило «списывается только разница с балансом» получается само. Подробно — [04-billing.md](../04-billing.md).
+22
View File
@@ -0,0 +1,22 @@
# D-010. Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам
Статус: принято, 20.10.2026.
## Контекст
Из России часто заходят через VPN, поэтому страна по IP не говорит, какая у пользователя валюта и какие карты. Карты РФ принимают одни платёжные системы, зарубежные — другие.
## Решение
1. Сущность «Валюты»: RUB и USD.
2. Сущность «Платёжные системы»: адаптер, валюты, назначения (оплата заказа, пополнение). RUB — процессинги для карт РФ (вероятно, KZ и TR), USD — Stripe и криптовалюта.
3. Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа показываются системы в валюте аккаунта.
4. У домена — включённые валюты, валюта по умолчанию и валюта для языка: ru → RUB, остальные → USD.
5. Переключатель валюты отдельный от языка: любая валюта с любым языком.
6. Цены тарифов — строкой на каждую валюту.
7. Валюта аккаунта задаётся при регистрации из выбранной и меняется, пока баланс нулевой и нет ключей. Дальше — через поддержку.
## Последствия
1. Маркетинговые страницы собираются в вариантах «язык × валюта» ([D-017](D-017-static-pages.md)).
2. Платёжная система в RUB доступна сразу на обоих брендах. Если понадобятся разные аккаунты одного провайдера для разных брендов, платёжной системе добавим необязательное поле «только для сайтов» (Q-17).
+17
View File
@@ -0,0 +1,17 @@
# D-011. Раздельные аккаунты по сайтам
Статус: принято, 20.10.2026.
## Решение
1. Пользователь привязан к сайту. Один email или Telegram на двух сайтах — два разных пользователя.
2. Внутри сайта: аккаунт можно создать по Telegram, почта привязывается кодом; на сайте есть «Войти через Telegram»; аккаунты объединяются по правилам [05-auth.md](../05-auth.md), раздел 6.4.
## Рассмотренные варианты
1. Общий аккаунт на оба бренда — бренды смешиваются в кабинете и в балансе.
2. Общий вход и отдельный профиль в каждом бренде — без дублей, но сложнее модель. Пользователь выбрал простоту.
## Последствия
Переход клиента с mass на dedicated — новая регистрация на veilix.app.
+18
View File
@@ -0,0 +1,18 @@
# D-012. Вход по коду через свою стратегию Payload; оформление заказа на my.*
Статус: принято, 20.10.2026.
## Решение
1. Входа по коду в Payload из коробки нет. Используем штатный механизм своих стратегий: коллекция `customers` с `disableLocalStrategy` и стратегией, которая читает нашу cookie сессии.
2. Коды и сессии — свои коллекции; коды и токены хранятся только как хэши.
3. Оформление заказа живёт на домене кабинета (my.*), а не на маркетинговом. Cookie сессии — только для хоста кабинета.
## Рассмотренные варианты
1. Better Auth или Auth.js поверх Payload — ещё одна система со своими таблицами и миграциями.
2. Cookie на весь домен бренда (`.veilix.app`) — тогда маркетинговые страницы перестают быть полностью кэшируемыми, а cookie видят dev-поддомены.
## Последствия
Маркетинговые сайты остаются без пользовательских cookies и полностью кэшируются.
+16
View File
@@ -0,0 +1,16 @@
# D-013. Капча ALTCHA
Статус: принято, 20.10.2026.
## Решение
ALTCHA — капча на proof-of-work, работает на нашем сервере, без третьих сторон. Вместе с лимитами по IP и email.
## Рассмотренные варианты
1. Google reCAPTCHA — передаёт данные пользователей в Google и строже проверяет пользователей VPN, то есть нашу аудиторию.
2. Cloudflare Turnstile — с июня 2025 российские провайдеры замедляют Cloudflare, пользователи из РФ могут не пройти проверку.
## Последствия
Провайдер капчи изолирован в `core/` и заменяется без переделки форм.
+18
View File
@@ -0,0 +1,18 @@
# D-014. Почта: шаблоны в Payload, переключаемый транспорт, без listmonk
Статус: принято, 20.10.2026.
## Решение
1. Шаблоны писем — в Payload, переведены, для каждого сайта свои.
2. Транспорт переключается в админке: SMTP, локальный sendmail (Postfix) или «только журнал».
3. Все письма пишутся в журнал. Флаг «Записывать коды входа в журнал» — для тестирования.
4. listmonk не используем.
## Рассмотренные варианты
listmonk — прежде всего сервис рассылок; переводов шаблонов в нём нет: на каждое письмо понадобилось бы 10 шаблонов (2 бренда × 5 языков).
## Последствия
Для отправки через Postfix нужны SPF, DKIM, DMARC и отправка с IP с правильной обратной записью ([09-infrastructure.md](../09-infrastructure.md), раздел 6).
+19
View File
@@ -0,0 +1,19 @@
# D-015. Боты на grammY без n8n; два бота на бренд
Статус: принято, 20.10.2026.
## Решение
1. Основной бот каждого бренда (команды, кнопка MiniApp, уведомления) написан в коде на grammY и работает внутри нашего приложения через вебхук. Тексты и кнопки — в Payload, переведены, правятся в админке.
2. Бот поддержки каждого бренда подключён к Chatwoot напрямую.
3. MiniApp — фронтенд в том же приложении, без n8n.
4. n8n не используем.
## Рассмотренные варианты
1. n8n как движок бота — визуальные схемы для покупок, ключей и баланса на 5 языках × 2 бота трудно поддерживать и тестировать, логика денег всё равно живёт в бэкенде.
2. Один бот на всё с пересылкой сообщений поддержки в Chatwoot через API — свой код, который нужно поддерживать.
## Последствия
Telegram отдаёт обновления бота только одному получателю, поэтому основной бот и бот поддержки — разные боты.
+15
View File
@@ -0,0 +1,15 @@
# D-016. Платёжные системы по валютам; автосписания — Stripe
Статус: принято, 20.10.2026. Провайдеры для RUB и криптовалюты выбираются на этапе 4 (Q-12).
## Решение
1. Каждый провайдер — адаптер с единым интерфейсом и флагами возможностей: автосписание, возврат, сохранение карты.
2. USD — Stripe (карты, сохранение карты, автосписания на переменную сумму, без Stripe Subscriptions) и криптовалюта.
3. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения, без автосписаний. ЮKassa — опционально, только пополнения.
4. Пользователи с картами РФ продлевают ключи с баланса; напоминание — за 3 дня.
5. Оплата внутри Telegram (Stars) — решаем на этапе 3.
## Последствия
На этапе 1 работает только `manual`. Модель данных готова к любому набору провайдеров.
+14
View File
@@ -0,0 +1,14 @@
# D-017. Статические маркетинговые страницы с пересборкой по событию
Статус: принято, 20.10.2026.
## Решение
1. Маркетинговые страницы собираются статически в вариантах «язык × валюта» и кэшируются.
2. Изменение в админке (цены, статус локации, FAQ, страницы) пересобирает затронутые страницы за секунды через пересборку по тегам. Запасной вариант — плановое обновление раз в 5 минут.
3. Наличие и цена перепроверяются на сервере при оформлении заказа.
4. Кабинет рендерится на сервере для каждого запроса.
## Последствия
Высокий балл PageSpeed, а устаревшая страница не может продать недоступную локацию.
+20
View File
@@ -0,0 +1,20 @@
# D-018. Umami на этом сервере и баннер cookies
Статус: принято, 20.10.2026.
## Решение
1. Аналитика — своя Umami без cookies, на этом сервере: Node.js и PostgreSQL здесь уже есть, а трекер проксирует этот же nginx.
2. Ставится из исходников, без Docker (официальный способ наряду с Docker-образом): `pnpm install`, `pnpm build`, `pnpm start`. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
3. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
4. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (`TRACKER_SCRIPT_NAME`, `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы.
5. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат). Необязательные скрипты — только после согласия.
## Рассмотренные варианты
1. Umami на VPS с Chatwoot и Twenty — трекеру пришлось бы ходить на другой сервер, где Docker и тяжёлые сервисы.
2. Яндекс Метрика и GA4 — передают данные третьим лицам и требуют согласия; нужны, только если будет реклама.
## Последствия
У Umami свой пользователь ОС, поэтому уязвимость в ней не открывает доступ к секретам Veilix.
+13
View File
@@ -0,0 +1,13 @@
# D-019. Chatwoot и Twenty CRM на отдельном VPS
Статус: принято, 20.10.2026.
## Решение
1. Chatwoot — один экземпляр на оба бренда: виджеты сайтов и боты поддержки как отдельные входящие каналы.
2. Twenty CRM — заявки из Payload уходят в Twenty через API.
3. Оба сервиса на отдельном VPS, ставит и обслуживает пользователь. Наше приложение работает с ними только по API.
## Последствия
До этапа 2 заявки видны в админке Payload и приходят письмом на почту поддержки.
+16
View File
@@ -0,0 +1,16 @@
# D-020. Модели ключей dedicated и mass
Статус: принято, 20.10.2026.
## Решение
1. Dedicated: ключ — индивидуальный IP в стране, выбранной при покупке. Страна не меняется никогда: нужна другая — покупается новый ключ. В ключ входит RU-адрес. Подключения: WG/AWG-конфиг на каждое устройство (до лимита тарифа), ссылка Happ или Xray, подключения к RU-адресу.
2. Mass: ключ включает все страны тарифа, страна не выбирается. Happ — одна ссылка со всеми странами. WG — конфиги любой страны тарифа: у подключения есть поле страны.
3. Тип ключа определяется полем `type` тарифа.
4. Подключения гибкие: у ключа любой набор подключений разных видов, поэтому ручная выдача на этапе 1 покрывает любой вариант.
5. Оплаченный период начинается с момента выдачи ключа.
6. На этапе 1 WG-конфиги mass — те, что добавил администратор, по странам. Нужна другая страна — через поддержку. Автоматически — на этапе 5.
## Последствия
Автоматическая выдача (этап 5) создаёт те же подключения, что сейчас добавляет администратор.
+15
View File
@@ -0,0 +1,15 @@
# D-021. Админка на plc.veilix.cc с доступом по IP
Статус: принято, 20.10.2026.
## Решение
1. Админка Payload и её REST API работают только на `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). На доменах брендов эти пути закрыты.
2. nginx пускает на хосты админки только разрешённые IP.
3. Свои эндпоинты приложения (ALTCHA, вебхуки, health) — под префиксом `/svc/`.
## Последствия
1. Адрес админки не связан с доменами брендов и закрыт от посторонних на уровне nginx.
2. Сайты, кабинеты и MiniApp к хосту админки не обращаются, поэтому ограничение по IP касается только администраторов ([02-architecture.md](../02-architecture.md), раздел 2).
3. Чтобы заходить в админку откуда угодно, в список разрешённых IP можно внести адреса своих VPN-серверов.
+27
View File
@@ -0,0 +1,27 @@
# Решения
По файлу на решение. Формат: контекст, решение, рассмотренные варианты, последствия. Новое решение — новый файл со следующим номером. Пересмотр — новый файл со ссылкой на старый, у старого статус «заменено».
| № | Решение | Статус |
|---|---|---|
| [D-001](D-001-single-app.md) | Одно Next.js-приложение на все домены | принято |
| [D-002](D-002-payload-version.md) | Payload 3, переход на 4 после стабильного релиза | принято |
| [D-003](D-003-hosting.md) | Без Docker: всё на хосте, процессы под systemd | принято |
| [D-004](D-004-environments.md) | Dev и prod на одном сервере под разными пользователями | принято |
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято |
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято |
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято |
| [D-008](D-008-uuid.md) | UUID и случайные публичные коды | принято |
| [D-009](D-009-billing-model.md) | Заказы, платежи, движения по счёту; всё через баланс | принято |
| [D-010](D-010-currencies.md) | Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам | принято |
| [D-011](D-011-accounts.md) | Раздельные аккаунты по сайтам | принято |
| [D-012](D-012-auth.md) | Вход по коду через свою стратегию Payload; оформление заказа на my.* | принято |
| [D-013](D-013-captcha.md) | Капча ALTCHA | принято |
| [D-014](D-014-email.md) | Почта: шаблоны в Payload, переключаемый транспорт, без listmonk | принято |
| [D-015](D-015-telegram-bots.md) | Боты на grammY без n8n; два бота на бренд | принято |
| [D-016](D-016-payments.md) | Платёжные системы по валютам; автосписания — Stripe | принято |
| [D-017](D-017-static-pages.md) | Статические маркетинговые страницы с пересборкой по событию | принято |
| [D-018](D-018-analytics.md) | Umami на этом сервере и баннер cookies | принято |
| [D-019](D-019-external-services.md) | Chatwoot и Twenty CRM на отдельном VPS | принято |
| [D-020](D-020-key-models.md) | Модели ключей dedicated и mass | принято |
| [D-021](D-021-admin-host.md) | Админка на plc.veilix.cc с доступом по IP | принято |