User-facing edge сервис Rezeis: Telegram-бот, Mini App и web-кабинет в одном образе
Релизы • Быстрый старт • Настройка окружения • Возможности • Архитектура
Reiwa — пользовательский edge/BFF-слой Rezeis. Это лицо сервиса для конечного клиента: один TypeScript-кодбейс отдаёт Telegram-бота, Telegram Mini App и web-кабинет (PWA). Reiwa не владеет реляционной БД — вся бизнес-истина (платежи, подписки, тарифы) живёт в rezeis-admin, а reiwa общается с ним по приватной сети через типизированный AdminClient.
Что выделяет Reiwa:
- 🧩 Один образ — три поверхности — API (Express BFF) + бот (grammY) + воркер + SPA в едином Docker-образе
- 🔌 BFF-дисциплина — провайдерские/админские вызовы только на сервере; в браузер не утекают Remnawave-UUID, ссылки провайдеров, токены и device-идентификаторы
- 📱 Source-aware redirect — оплата возвращает пользователя туда, откуда он пришёл (Mini App ↔ web)
- 💳 Единый платёжный UX — покупка, продление, улучшение и доп-опции одинаковыми полноэкранными мастерами
- 🗑 Самостоятельное удаление подписки long-press'ом по карточке с отзывом доступа
- 🎨 PWA-кабинет на React 19 с операторским брендингом: 104 концептуальные темы, настраиваемые карточки и безопасные GPU/CSS-эффекты
- 🌍 Полная i18n (ru/en) — ноль захардкоженных строк
- 🔐 Redis-сессии, rate-limit, CSRF/Origin-защита, fail-closed в проде
GitHub Container Registry публикует единый образ при каждом push'е в main и при каждом теге v*, но latest двигает только тег.
# Latest release (moves only when a v* tag is pushed)
docker pull ghcr.io/dizzzable/reiwa:latest
# Pin to a specific release
docker pull ghcr.io/dizzzable/reiwa:v0.9.7.63Доступные теги: latest (последний выпуск — двигается только push'ем тега v*), v0.9.7.63 (тег релиза), main (текущая ветка main, ещё не выпуск), плюс sha-<short> для каждого собранного коммита. Прод-docker-compose.yml использует latest, то есть получает только выпуски.
Один образ обслуживает всё: API на
REIWA_PORT(по умолчаниюnode dist/api/main.js) раздаёт собранную SPA из/app/web, бот —dist/bot/main.js, воркер —dist/worker/main.js. Роль выбирается командой запуска контейнера.
Образ тянется из GHCR — исходники на сервере не нужны, только два файла.
# 1. Каталог установки
mkdir -p /opt/reiwa && cd /opt/reiwa
# 2. Скачать compose и шаблон окружения
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/dizzzable/reiwa/main/docker-compose.yml
curl -fsSL -o .env https://raw.githubusercontent.com/dizzzable/reiwa/main/.env.example
# 3. Общая docker-сеть с rezeis (создать, если Remnawave/rezeis-стек её ещё
# не создал — нужна для связи reiwa ↔ rezeis по имени `rezeis:8000`):
docker network create remnawave-network 2>/dev/null || true
# 4. Сгенерировать ЛОКАЛЬНЫЙ секрет прямо в .env (он нужен только reiwa):
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -hex 16)|" .env
# 5. Дозаполнить вручную:
# REIWA_DOMAIN публичный домен кабинета (app.example.com)
# REZEIS_HOST=rezeis имя контейнера админки на одном VPS (или panel.example.com при split)
# REZEIS_TOKEN API-токен, созданный в админке rezeis (Настройки → API-токены)
# REZEIS_WEBHOOK_SECRET = WEBHOOK_SECRET_HEADER из rezeis (если включаете push)
# REZEIS_INTERNAL_SHARED_SECRET
# = REZEIS_INTERNAL_SHARED_SECRET из rezeis, тот же
# самый. НЕ генерируйте его здесь: панель проверяет
# этим значением подпись каждого запроса от reiwa.
# Сгенерируйте один раз (openssl rand -hex 32) и
# вставьте одну и ту же строку в ОБА .env.
# BOT_TOKEN, BOT_USERNAME от @BotFather
nano .env
# 6. Запуск (reiwa + reiwa-bot)
docker compose up -dВ продакшене сервис fail-closed: без Redis или без гарантии secure-cookie
запуск останавливается. Ослабить только осознанно — REIWA_ALLOW_DEGRADED=true /
REIWA_ALLOW_INSECURE_COOKIES=true. Полный разбор переменных и таблицу
совпадений с rezeis — см. docs/environment.md.
Обновление:
cd /opt/reiwa && docker compose pull && docker compose up -dReverse proxy: на одном VPS с rezeis удобнее единый Caddy — см.
caddy-combined в репозитории rezeis. Отдельные конфиги — в deploy/proxies/.
Раздельная установка сама по себе ничего дополнительного не требует: REZEIS_HOST
в .env указывает на публичный домен панели, и обращения к ней идут через обычный
DNS.
Но если резолвер у провайдера подводит, getaddrinfo EAI_AGAIN кладёт все
бизнес-операции разом — запросы висят до таймаута и отвечают 503, а в логе панели
при этом пусто: они до неё не дошли. Оператору приходит карточка про отменённый
запрос, в которой про DNS не сказано ничего; настоящая причина видна только в логе
контейнера:
WARN: [api] Upstream temporarily unreachable
err: getaddrinfo EAI_AGAIN <домен панели>
Чинится на хосте кабинета, а не в compose — резолверами демона Docker.
daemon.json может уже существовать (драйвер логов, зеркала реестра,
storage-driver). Не перезаписывайте его целиком — потеряете эти настройки.
Сначала посмотрите, что там:
sudo cat /etc/docker/daemon.json 2>/dev/null || echo "файла нет"Если файла нет — создайте:
echo '{ "dns": ["1.1.1.1", "8.8.8.8"] }' | sudo tee /etc/docker/daemon.jsonЕсли файл есть — добавьте в него только ключ dns, сохранив остальное.
С резервной копией, потому что ошибка в JSON не даст демону подняться:
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak
sudo jq '. + {"dns": ["1.1.1.1", "8.8.8.8"]}' /etc/docker/daemon.json.bak | sudo tee /etc/docker/daemon.jsonНет jq — поставьте (sudo apt install -y jq) либо впишите ключ руками:
верхним уровнем объекта, рядом с существующими, не трогая их.
Перед рестартом убедитесь, что JSON цел — с битым файлом демон не запустится, и поднимать его придётся уже из резервной копии:
python3 -m json.tool /etc/docker/daemon.json > /dev/null && echo "JSON валиден"Затем перезапустите демон:
sudo systemctl restart docker"live-restore": true в том же файле его избегает, но
на смену настроек DNS всё равно нужен рестарт.)
Проверка, что помогло — резолвим домен панели изнутри самого контейнера, то есть ровно так, как это делает приложение:
docker exec reiwa node -e "require('dns').resolve4(process.env.REZEIS_HOST, (e, a) => console.log(e ? 'ОШИБКА: ' + e.code : 'ОК: ' + a))"Должно напечатать ОК: и адрес. ОШИБКА: EAI_AGAIN означает, что резолверы всё
ещё не отвечают.
Почему именно демон, а не dns: в compose. Ключ dns: у сервиса замещает
встроенный резолвер Docker (127.0.0.11), а это он превращает REZEIS_HOST=rezeis
в адрес контейнера. На одиночном VPS панель после такого перестанет находиться
вообще. Настройка демона так не ломает: встроенный резолвер остаётся на месте и
лишь пересылает внешние имена новым адресам — поэтому чинятся сразу и панель, и
api.telegram.org, и оба сервиса кабинета (reiwa и reiwa-bot, который ходит к
панели сам).
Прод-docker-compose.yml ссылается только на готовый образ. Чтобы собрать
из исходников локально — build-оверлей:
git clone https://github.com/dizzzable/reiwa.git && cd reiwa
cp .env.example .env # заполнить секреты
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build --force-recreate reiwa reiwa-bot«Горячая» разработка (bind-mount + watch/HMR) — docker-compose.dev.yml,
либо без docker:
npm install
npm run dev:api # Express BFF + раздача SPA
npm run dev:bot # grammY Telegram bot
npm run dev:worker # фоновые задачи
cd web && npm install && npm run dev # фронт (отдельный терминал)Системные требования: Node.js 26+, Redis/Valkey 8+, доступный экземпляр rezeis-admin.
- grammY с single-screen контрактом (edit-in-place — экран перерисовывается, а не засоряет чат)
- Определение локали, operator-managed конфиг-кэш, баннеры и динамические экраны из Bot Studio
/start payment_return— возврат после оплаты из Mini App- Команды: меню, подписки, покупка, промокод, рефералы, помощь и переходы в Mini App
Один React 19 + Vite PWA работает и как Mini App внутри Telegram, и как самостоятельный web-кабинет:
- Dashboard — карусель карточек подписок с брендингом оператора: градиент, паттерн, логотип, геометрия, стекло, текст и анимированный фон; полоса трафика, срок и устройства
- Покупка — мастер: тариф → срок → платёжная система → оплата
- Продление — мульти-выбор своих подписок, единый платёж, учёт скидок и промокодов
- Улучшение тарифа — выбор подписки → новый тариф → срок → новая цена
- Доп-опции — полноэкранный мастер: подписка → опция (трафик/устройства) → оплата; бесплатные активируются мгновенно
- Удаление подписки — long-press по карточке → подтверждение → отзыв доступа (финально, без возврата)
- Устройства — список HWID, отзыв, перегенерация ссылки
- Рефералы / партнёрка, обмен баллов, промокоды, тикеты поддержки, FAQ, настройки, уведомления
- Онбординг-тур с подсветкой реальных элементов (включая жест удаления)
- Дополнительные услуги — покупка и управление расширениями тарифа, включая трафик и устройства
- Уведомления — лента событий, настройки категорий, переход к связанному действию и отметка всех сообщений прочитанными
- Поддержка — обращения из кабинета и гостевой вход в поддержку до авторизации; FAQ поддерживает операторские медиа-материалы
- AI-помощник — пользовательская точка входа в поддержку, когда оператор включает её в Rezeis
- Программа роста — реферальные ссылки, партнёрский статус, баллы, промокоды, задания и рекламные предложения, если они активированы оператором
- Настройки кабинета — язык, PWA-установка, уведомления и разрешённая оператором светлая/тёмная вариация бренда
- Telegram Mini App: подписанный
initDataавтоматически создаёт или восстанавливает web-сессию; пользователь, пришедший из бота, не должен видеть форму логина - Обычный браузер: вход по логину/паролю, существующей cookie-сессии или одноразовой ссылке возврата
- Единый аккаунт: Telegram-first пользователь может создать web-учётную запись или привязать существующую, чтобы один кабинет работал и в Telegram, и в браузере
- Внешняя авторизация: на странице входа отображаются только включённые оператором способы OAuth
Reiwa не хранит платежи — он проксирует к rezeis-admin, который владеет 15 шлюзами. На стороне reiwa:
- Единый выбор платёжной системы с настоящими SVG-иконками провайдеров и валют
- Source-aware return URL: Mini App →
t.me/<bot>?start=payment_return, web →/payment-return - Поллинг статуса оплаты + анимированный экран возврата
- Redis-сессии,
express-rate-limit, CSRF/Origin allow-list, request-id трейсинг, Helmet - Brute-force detection с backoff
- Типизированный
UpstreamError(status/body) — апстрим-ошибки логируются, но не форвардятся в браузер - Наружу не выходят сырые Remnawave-UUID, ссылки провайдеров, токены, device/Telegram-идентификаторы — только стабильные safe-labels и opaque public id
Полная поддержка ru/en через react-i18next. Все пользовательские строки — в web/src/i18n/{ru,en}.ts; ноль захардкоженных строк (бренды и формат-локали дат — исключение).
vite-plugin-pwa (injectManifest) + собственный service worker (web/src/sw.ts): прекэш ассетов, офлайн-старт, устанавливаемость.
Внешний вид задаёт оператор в Rezeis → WEB Reiwa; Reiwa получает уже разрешённые токены брендинга через публичный bootstrap-конфиг и не зависит от каталога админ-панели во время отрисовки. При кратковременной ошибке обновления браузер сохраняет последнюю валидную конфигурацию, поэтому кабинет не должен мигать стандартной темой.
- Пресеты: восемь исторических тем и каталог из 104 концептуальных пресетов. Концепт передаёт палитру, фон приложения, типографику, поверхности, радиусы, градиент/паттерн и параметры карточки.
- Светлый/тёмный режим: оператор выбирает сам концепт и режим по умолчанию. При политике
user-selectableпользователь может сменить только светлую/тёмную вариацию этого же концепта — выбрать другой операторский пресет он не может. - Карточки подписок: глобальные параметры являются базой для всех карточек. Для позиций можно создать до 20 слотов: новый слот наследует глобальный эффект, а отдельный режим
overrideосознанно меняет эффект и его параметры только для этой позиции. Статический градиент слота независим от режима эффекта. - Читаемость и композиция: режим текста (
auto, светлый, тёмный или свой цвет) и стеклянный слой настраиваются отдельно от эффекта. Изменение контраста не должно подменять градиент или палитру, выбранные оператором. - Безопасный рендер: градиент и паттерн всегда являются базовым слоем. Если браузер или GPU не поддерживает выбранный Canvas/WebGL-эффект, Reiwa показывает CSS-версию того же эффекта с операторской палитрой и прозрачностью, а не подменяет его чужим эффектом или пустой карточкой.
reiwa/
├── src/ # TypeScript edge (3 entrypoints)
│ ├── api/ # Express BFF
│ │ ├── routes/ # /api/v1/* (subscription, payments, gateways, ...)
│ │ ├── middleware/ # session, user-identity, CSRF/Origin, rate-limit
│ │ └── lib/ # safe error responses
│ ├── bot/ # grammY Telegram bot (single-screen contract)
│ ├── worker/ # background runtime (scheduled work)
│ ├── infrastructure/
│ │ ├── admin-client/ # AdminTransport (undici pool, bearer + HMAC)
│ │ │ └── namespaces/ # типизированный фасад над internal API rezeis-admin
│ │ ├── redis/ # Redis-backed web sessions
│ │ ├── bot-config/ # operator-managed конфиг бота
│ │ └── i18n/ # translator + locale packs (ru/en)
│ ├── core/ # zod-config, errors, enums, version
│ └── lib/ # client-source, payment-return-url, helpers
├── web/ # React 19 + Vite PWA (Mini App + web cabinet)
│ └── src/
│ ├── features/ # экран-в-папке (lazy-loaded)
│ ├── components/ # shared UI (Radix + reactbits/WebGL)
│ ├── hooks/ # useLongPress, onboarding tour, session, ...
│ ├── stores/ # Zustand (purchase / renewal / upgrade / addons)
│ ├── assets/{payments,currency}/ # настоящие SVG-иконки
│ ├── i18n/ # ru.ts + en.ts
│ └── lib/ # api-client, client-source, utils
├── deploy/proxies/ # caddy / nginx / angie / traefik edge-стек
├── Dockerfile # unified image (API + bot + worker + SPA)
├── docker-compose.yml # production stack
└── .github/workflows/docker-publish.yml # CI: единый образ → GHCR
| Технология | Версия | Назначение |
|---|---|---|
| Node.js | 24 | Runtime |
| TypeScript | 6 | Type safety |
| Express | 5 | BFF / HTTP-сервер |
| grammY | 1.42 | Telegram-бот |
| undici | 8 | HTTP-клиент к rezeis-admin (persistent pool) |
| ioredis | 5 | Сессии, rate-limit, brute-force |
| zod | 4 | Валидация конфига и входных данных |
| pino + pino-http | — | Структурное логирование + request-id |
| helmet | 8 | Security headers |
| vitest + fast-check | 4 | Unit + property-based тесты |
| Технология | Версия | Назначение |
|---|---|---|
| React | 19 | UI framework |
| TypeScript | 6 | Type safety |
| Vite | 8 | Build tool (rolldown) |
| TanStack Query | 5 | Server state |
| Zustand | 5 | Client state (wizard stores) |
| Radix UI | — | Component primitives |
| Tailwind CSS | 4 | Styling |
| three / @react-three/fiber | — | WebGL-фон карточек |
| Motion | 12 | Анимации |
| react-i18next | — | i18n (ru/en) |
| vite-plugin-pwa + workbox | 1.3 | PWA / service worker |
| sonner | — | Тосты |
| Технология | Назначение |
|---|---|
| Docker + Compose | Контейнеризация (единый образ) |
| Caddy / Nginx / Angie / Traefik | Reverse proxy (TLS 443) |
| GitHub Actions | CI/CD |
| GHCR | Container registry |
Все настройки задаются через окружение и валидируются один раз на старте в app.config.ts (zod). Скопируйте .env.example → .env и заполните change_me.
| Переменная | Обязательная | Описание |
|---|---|---|
REIWA_DOMAIN |
✅ | Публичный домен — драйвит CORS/CSRF allow-list, webApp-кнопки и payment-return ссылки бота |
REIWA_HOST, REIWA_PORT |
— | Хост/порт API (раздаёт также SPA в режиме единого образа; по умолчанию 0.0.0.0:5000) |
BOT_TOKEN |
✅ | Токен Telegram-бота |
BOT_USERNAME |
✅ | Username бота без @ — для t.me/<bot>?start=payment_return |
BOT_MINI_APP, BOT_SUPPORT_USERNAME |
— | Режим Mini App и контакт поддержки |
REZEIS_HOST, REZEIS_PORT |
✅ | Адрес внутреннего API rezeis-admin (источник истины) |
REZEIS_TOKEN |
✅ | Bearer для вызовов rezeis-admin |
REZEIS_INTERNAL_SHARED_SECRET |
✅ (prod) | HMAC-подпись запросов reiwa→admin и хопа reiwa-api→bot (≥32 симв.). Должен быть идентичен одноимённой переменной в rezeis — панель проверяет подпись именно им |
REZEIS_WEBHOOK_SECRET |
✅ | Проверка входящих вебхуков от rezeis-admin (X-Rezeis-Signature) |
REDIS_HOST, REDIS_PORT, REDIS_NAME, REDIS_PASSWORD |
✅ | Redis для сессий / rate-limit / FSM бота |
REIWA_COOKIE_SECURE |
true в production |
Secure-флаг session-cookie; cookie хранит opaque UUID, валидируемый через Redis |
REIWA_ALLOW_DEGRADED |
— | true — разрешить старт при недоступном Redis (по умолчанию fail-closed) |
REIWA_ALLOW_INSECURE_COOKIES |
— | true — разрешить небезопасные cookie (только осознанно) |
Полный список с комментариями — в .env.example.
Edge-поверхность, которую потребляет кабинет (все эндпоинты за сессией):
/api/v1/session— сессия пользователя/api/v1/subscription,/subscriptions/all— подписки пользователя/api/v1/subscription/renewal-options,/upgrade-options— опции продления/улучшенияDELETE /api/v1/subscription/:id— самостоятельное удаление подписки/api/v1/gateways— активные платёжные системы/api/v1/payments/checkout,/renewal-checkout— создание оплаты (source-aware)/api/v1/payments/:id— статус оплаты/api/v1/add-ons/plan/:planId,/add-ons/purchase— доп-опции/api/v1/devices/*— устройства (список, отзыв, перегенерация ссылки)/api/v1/internal/metrics— health/метрики reiwa для дашборда rezeis-admin
# Edge (backend)
npm run check # TypeScript no-emit (0 errors policy)
npm test # vitest unit suite
npm run test:pbt # property-based (node:test + fast-check)
# Frontend
cd web
npm run build # tsc -b + vite buildВ reiwa ESLint не настроен — гейты держим через
tsc+ тесты + сборку.
Единый Dockerfile собирает и обслуживает:
dist/api/main.js— Express BFF (раздаёт SPA из/app/web)dist/bot/main.js— Telegram-ботdist/worker/main.js— фоновый воркерweb/dist/— собранная PWA
Образ публикуется автоматически в GHCR через .github/workflows/docker-publish.yml: push в main даёт теги main и sha-<short>, push тега v* — тег версии, latest и sha-<short>. CI собирает один unified-образ (не отдельные backend/web).
deploy/proxies/ содержит Remnawave-style reverse-proxy стеки (caddy / nginx / angie / traefik), фронтящие кабинет по 443 с bring-your-own сертификатом — см. deploy/proxies/README.md.
Reiwa и rezeis-admin версионируются в lockstep (одинаковая версия в обоих репозиториях). Reiwa — это лицо для пользователя; админ-истина и операционные инструменты остаются в rezeis-admin. Reiwa не является копией Remnawave Panel — это Rezeis-owned сервис с интеграцией Remnawave за admin-owned швами.
Полные release notes по каждой версии — на странице GitHub Releases с детальным changelog.
MIT — свободное использование, модификация и распространение.
- Fork репозитория
- Создайте feature-ветку:
git checkout -b feature/your-feature - Commit по Conventional Commits:
feat(scope): description - Push и откройте Pull Request с описанием изменений
Перед PR: npm run check, npm test и npm run build (в web/) должны проходить без ошибок.