Основной конфиг (config.<mode>.yml)
Конфигурация разделена на несколько областей:
app— параметры самого приложения (сервер, HTTP-клиенты, флаги безопасности, сессии, очистка кэша, модули).acp— авторизация ACP через HELM service token.infrastructure— подключения к внешним сервисам: PostgreSQL, Redis, Cloudflare и платформы.domain— бизнес-настройки (стримы, ивенты, конкурсы, соперники, user-data, очистка, заказы, кланы и войны кланов).observability— метрики и интеграция Prometheus.tasks— системные cron-задачи.alerts— назначения и шаблоны оповещений.
Ниже описан каждый блок.
1. app
app.server
app:
server:
id: 1 # произвольная строка/число, попадает в X-Server-Id
host: 0.0.0.0 # слушаемый адрес
port: 8080 # порт HTTP-сервераapp.http.timeouts
Таймауты для общего HTTP-клиента, используются стримами, traffic-flow и т.д. Формат — time.Duration.
app:
http:
timeouts:
request: 30s
dial: 20s
keep_alive: 20s
tls_handshake: 10s
response_header: 10sapp.security
app:
security:
secret: "..." # главный секрет (подписи платформ и т.д.)
disable_auth: false # глобально выключить проверку платформенных токенов
enable_cors: false # включить permissive CORS middleware (только для dev/тестов)
max_body_size: 4194304 # лимит тела запроса в БАЙТАХ (integer); 4194304 = 4 MB
rate_limit:
enabled: false # НЕ рекомендуется за Cloudflare/Nginx — используйте WAF Rate Limiting
rps: 20 # запросов в секунду на IP (ненадёжно за reverse-proxy)
burst: 40 # максимальный burst на IP
jwt:
secret: "..." # отдельный ключ для подписи сессионных JWT (HMAC-SHA256)
ttl: 24h # время жизни токенаRate limiter хранится in-memory; при превышении возвращается HTTP 429 Too Many Requests.
jwt.secret — отдельный ключ, используемый для подписи и верификации JWT-токенов V2 сессий. Может быть использован другими сервисами для проверки токена. jwt.ttl — время жизни токена (формат time.Duration).
app.security.traffic_flow_webhook_secret
Секрет для проверки входящих вебхуков traffic-flow. Значение сравнивается с заголовком X-Webhook-Secret при обработке /ext/traffic-flows/webhook (constant-time сравнение); при несовпадении запрос отклоняется с unauthorized.
app:
security:
traffic_flow_webhook_secret: "..."Эндпоинт fail-closed. Пустой секрет не означает «проверка выключена»: вебхук начисляет награды, поэтому при незаданном traffic_flow_webhook_secret он отвечает unauthorized на любой запрос и пишет об этом в лог. Чтобы вебхук работал, секрет обязателен.
Ключ объявлен в
infrastructure/config/schemas/config.schema.json(app.security.traffic_flow_webhook_secret), поэтому его можно задавать прямо в YAML. Значение — секрет, так что в репозитории его держат пустым и подставляют через переменную окруженияMARV__APP__SECURITY__TRAFFIC_FLOW_WEBHOOK_SECRET.
app.session.single_mode (DEPRECATED)
Deprecated: single-session mode — это legacy V1 функциональность. V2 использует исключительно JWT-авторизацию (
Authorization: Bearer). Эти параметры будут удалены в будущих версиях.
app:
session:
single_mode:
enabled: false # V1 only: если true — выдаём X-Set-Session-Token и проверяем X-Session-Token
blocking: false # V1 only: если true — блокируем запрос без актуальной сессииapp.online_ttl
Время, в течение которого пользователь считается «онлайн» после последнего запроса. По нему OnlineTracker отсекает старые отметки в наборе присутствия marv:persist:online_users и выставляет набору срок жизни.
app:
online_ttl: 5m # формат time.Duration; по умолчанию 5mapp.scheduler
Настройки планировщика cron-задач.
app:
scheduler:
task_timeout: 1h # формат time.Duration; таймаут на выполнение одной задачиtask_timeout — предельное время выполнения отдельной cron-задачи (infrastructure/scheduler/manager.go). По истечении контекст задачи отменяется.
app.cache_clear
app:
cache_clear:
enabled: true # чистить volatile-кэши при старте контейнера
patterns:
- "marv:cache:users:*"
- "marv:cache:streams:*"app.invitation_awards
Строки с JSON-наградой для получателя/отправителя инвайта:
app:
invitation_awards:
receiver: '{"currency":"coins","amount":100}'
sender: '{"currency":"coins","amount":50}'Зарезервировано / не подключено. Ключ присутствует в схеме, но в рантайме сейчас не читается: единственный потребитель —
UserService.HandleInviterBonus— не имеет вызывающих сторон, поэтому награды по инвайтам не начисляются. Заполнение этих строк на текущее поведение не влияет.
app.modules
Список модулей для загрузки. Позволяет управлять набором функционала через конфиг (без изменения кода):
app:
modules:
- experiment
- event
- clan
- message
- bots
- product
- remote_config
- rival
- stream
- ads
- alert
- order
- traffic_flow
- transaction
- worldМодули system, user, cron_task, merchant, platform создаются по умолчанию и не нуждаются в перечислении; остальные — только если указаны. Полный набор допустимых значений (см. схему config.schema.json): ads, alert, bots, clan, cron_task, event, experiment, merchant, message, order, platform, product, remote_config, rival, stream, system, traffic_flow, transaction, user, world.
app.routes
Включение/отключение групп HTTP-маршрутов:
app:
routes:
acp: true # Admin Control Panel
api: true # группа маршрутов /api
ext: true # внешние вебхуки (мерчанты)
v1: true # V1 API (deprecated)
v2: true # V2 API (основной)В config.example.yml включены все группы; отключите ненужные, выставив false.
app.min_version
Минимальные версии клиента по маршрутам. Ключ — regex для HTTP-пути, значение — минимальная версия (semver). Заголовок X-Client-Version проверяется middleware; если версия ниже — возвращается ошибка.
app:
min_version:
"^/v2": "1.2.0" # все /v2/* требуют клиент >= 1.2.0
"^/v2/streams": "1.5.0" # стримы требуют >= 1.5.0Если секция пуста или отсутствует — проверка версий отключена.
Прочие поля
app.log_dir— каталог для файловых логов (infrastructure/logger).
Ключи, разрешённые схемой, но не читаемые кодом
Схема запрещает лишние ключи (additionalProperties: false), поэтому удалить их значило бы сломать валидацию у уже развёрнутых конфигов. Они остаются разрешёнными и ни на что не влияют:
| Ключ | Почему остался |
|---|---|
app.http.timeouts.handler | Таймаут-middleware удалён |
app.cdn_url | Не читается кодом |
app.invite_bonus | Легаси-ключ, заменён на app.invitation_awards |
app.validation.strict | Конфиг всегда проверяется этой схемой |
app.invitation_awards | См. выше: единственный потребитель не вызывается |
2. Авторизация ACP
acp (HELM service token)
ACP-запросы аутентифицируются токеном, выпущенным HELM (RS256 JWT, iss: helm). MARV проверяет подпись по публичным ключам HELM и извлекает per-env доступ оператора:
acp:
access_key: "your-project/environment" # ключ HELM: <project-slug>/<base-name>
jwks_url: "https://helm.example.com/v1/auth/jwks" # откуда тянуть публичные ключи HELM
jwks: "{}" # кэш JWKS; заполняется задачей helm_certsEmail оператора берётся из claim sub; битовая маска доступа — из access["<access_key>"] (0, если ключа нет). jwks периодически обновляется системной задачей helm_certs.
Legacy Google OAuth-вход в ACP удалён (
oauth.google/ задачаgoogle_certsбольше не существуют) — авторизация ACP идёт исключительно через HELM service token.
3. infrastructure
infrastructure.postgres
infrastructure:
postgres:
host: localhost
port: 5432
username: app
password: secret
database: marv
pool:
max_open: 20
max_idle: 10
timeout: 300s
sslmode: disable
dry_run: false
# Опциональная read-реплика: SELECT'ы идут на неё, запись/транзакции — на primary.
# Опустите блок для одного пула. Creds/db/sslmode наследуются от primary; нужен только host.
# read:
# host: replica.internal
# pool: { max_open: 40, max_idle: 20 }infrastructure.redis
infrastructure:
redis:
address: 127.0.0.1:6379
password: ""
db: 0
pool: 10
timeout: 300s # используется как pool-timeout и default TTL
store_ttl: 4h # TTL для Set() без явного timeoutinfrastructure.cloudflare
Учетные данные для Cloudflare API.
infrastructure:
cloudflare:
account_id: "..."
api_token: "..."
creator_prefix: "example" # префикс имени для загружаемых в Cloudflare Stream креативовinfrastructure.platforms
Словарь платформ. Ключ — строковый api_type (числовой идентификатор платформы).
infrastructure:
platforms:
"2":
type: vk
credentials:
id: "7365134"
secret_key: "..."
"30":
type: ms_start
credentials:
id: "9NPZT17FTVZ4"
package_name: "HGPOINTLTD.MergeHotel"
secret_key: "..."
merchant_profiles:
default:
type: xsolla
merchant_id: "702055"
project_id: "265997"
api_key: "..."
webhook_secret: "..."
promo:
type: xsolla
merchant_id: "702055"
project_id: "400001"
api_key: "..."
webhook_secret: "..."type — тип платформы. Допустимые значения (enum схемы config.schema.json): default, unauthorized, ok, vk, fb, fb_login, mm, mobage, dr, fs, gm, app_store, play_market, amazon, ms_store, ru_store, bazaar, galaxy_store, yandex, exe, beeline, play_deck, ms_start, tg, crazy_games, discord, myket, orbit, jest, google_signin, apple_signin, game_center, play_games.
name (опционально) — человекочитаемое имя платформы.
credentials — учётные данные платформы. Набор полей зависит от типа платформы:
| Поле | Описание |
|---|---|
id | Идентификатор приложения |
app_id | Альтернативный ID приложения |
uid | UID приложения |
secret_key | Секретный ключ |
application_key | Ключ приложения (OK) |
auth_token | Токен авторизации |
package | Имя пакета |
package_name | Имя пакета (MS Store, RuStore) |
client_id | Client ID (OAuth) |
refresh_token | Refresh Token (Bazaar) |
redirect_url | Redirect URL (OAuth) |
consumer_key | Consumer Key (Mobage) |
cert | Сертификат |
public_key | Публичный ключ (RuStore) |
private_key_id | ID приватного ключа (RuStore, App Store) |
issuer_id | Issuer ID команды в App Store Connect (App Store) |
company_id | ID компании (RuStore) |
bot_token | Токен бота (Telegram) |
App Store (type: app_store) проверяет покупку через App Store Server API: сервер запрашивает у Apple транзакцию по transaction_id, который прислал клиент, и проверяет подписанный ответ Apple. Чек приложения (receipt) не используется. Учётные данные:
| Поле | Значение |
|---|---|
id | Bundle ID приложения, например com.happygames.mergecafe |
private_key_id | Key ID ключа In-App Purchase: App Store Connect → Users and Access → Integrations → Keys: In-App Purchase. Ключ App Store Connect API (Team/Individual Keys) не подходит |
issuer_id | Issuer ID команды, вверху той же страницы |
secret_key | Сам ключ (.p8) в PEM или в base64 от PEM. Хранится в Vault и приходит через MARV__INFRASTRUCTURE__PLATFORMS__<api_type>__CREDENTIALS__SECRET_KEY |
У ключа In-App Purchase нет роли или уровня доступа: при создании задаётся только имя. Ключ действует на все приложения команды, а конкретное приложение выбирается bundle id в запросе. Подходит только для App Store Server API, Advanced Commerce API и External Purchase Server API. .p8 можно скачать один раз.
"10":
type: app_store
credentials:
id: "com.happygames.mergecafe"
private_key_id: "ABC123DEFG"
issuer_id: "57246542-96fe-1a63-e053-0824d011072a"
secret_key: "" # из VaultПокупка принимается, только если она сделана в этом приложении, совпадает с SKU, который назвал клиент, не возвращена и (для подписки) не истекла. Сервер сначала ищет транзакцию в production, затем в Sandbox: так проходят покупки App Review и TestFlight. Sandbox-покупка засчитывается, но в журнале помечена sandbox = true и выручкой не считается. Цену и валюту записывает Apple. Одна транзакция App Store засчитывается одному аккаунту: если её прислал другой аккаунт, сервер отвечает ошибкой iap.order_taken.
merchant_profiles — словарь профилей мерчанта. Ключ — произвольное имя профиля (используется в маршрутах /ext/{api_type}/{profile}/{action}).
Поля профиля:
| Поле | Обязательное | Описание |
|---|---|---|
type | да | Тип мерчанта (xsolla, rustore, ...) |
merchant_id | нет | ID мерчанта |
project_id | нет | ID проекта |
api_key | нет | API-ключ |
webhook_secret | нет | Секрет для подписи вебхуков |
company_id | нет | ID компании (RuStore) |
private_key_id | нет | ID приватного ключа (RuStore) |
Для Xsolla Basic Auth (merchant_id:api_key) вычисляется автоматически при каждом запросе.
Профиль default используется, если в маршруте не указан конкретный профиль.
4. domain
domain:
stream:
delete_after: 1080h
entrance_timeout: 900s
proxy_url: "https://..."
subdomain: "example.cloudflarestream.com"
events:
lookback: 336h
dead_letter_queue:
enabled: true
max_retries: 3
contest:
group_size: 50
rivals:
max_count: 100
compatible_api_types:
vk: [1,2,3]
user_updates:
batch_size: 250
ttl: 168h # TTL записей в очереди обновлений
cleanup:
data_retention: 4320h # хранить данные user/world столько (180d); используется задачей data_clean
orders:
ttl_days: 7 # через сколько дней удалять pending-заказы (cron order_cleanup)
clans:
member_cap: 50 # дефолтный размер ростера нового клана
join_request_limit: 5 # макс. активных заявок на игрока
snapshot_min_write_interval: 60s # rate-limit записи живого снапшота (анти-bloat)
leader_inactive_succession: 336h # авто-передача лидерства при простое лидера (14d)
inactive_autokick: 720h # мягкий авто-кик неактивных участников (30d)
badwords_file: "" # путь к текстовому блоклисту имени/тега (одно слово на строку, # — коммент); пусто = выключено
clan_war:
participation_mode: auto # auto | opt_in
min_members: 1
power_band_pct: 30 # подбор: противники в пределах этого % силы
attacks_per_player: 2
retention_wars: 2 # сколько прошлых войн отдавать в истории
retention: 720h # GC: удалять finished-войны, чей конец завершился раньше (30d)
attack_reserve_timeout: 180s # держать зарезервированную атаку до просрочки
attack_reserve_on_expire: refund # refund | consume — что делать с атакой при просрочке
phases: # упорядоченная последовательность фаз; каждая длится свой duration
- { type: points, duration: 24h, winning_points: 1 }
- { type: points, duration: 24h, winning_points: 2 }
- { type: points, duration: 24h, winning_points: 2 }
- { type: points, duration: 24h, winning_points: 2 }
- { type: points, duration: 24h, winning_points: 2 }
- { type: battle, duration: 24h, winning_points: 4, points_per_win: 1 }
- { type: results, duration: 24h }
tier: # tier_points клана (счётчик побед в войнах) → тир
points_per_win: 1
points_per_loss: 0
thresholds: { E: 0, D: 5, C: 10, B: 15, A: 20, "S+": 25, "S++": 35, "S+++": 50 }
rewards: # числа-плейсхолдеры (баланс — на стороне гейм-дизайна)
delivery: mail # mail: сервер кладёт в почту | claim: клиент забирает через clans/war/rewards
min_points_for_reward: 1 # меньше этого — никакой общей награды (ни win, ни lose)
min_points_for_win: 100 # win-награда только участникам, набравшим больше
common: # общая награда пер-тир: <tier>: { win: {...}, lose: {...} }
E: { win: { gold: 1000 }, lose: { gold: 500 } }
"S+": { win: { gold: 5300, tickets: 7800 }, lose: { gold: 2650, tickets: 3900 } }
individual: # кумулятивная лестница (до 25 стадий): набрал points → выдаётся reward
- { points: 500, reward: { gold: 100 } }
- { points: 1000, reward: { gold: 200 } }
clan_chat:
enabled: true # включён ли модуль чата
max_message_length: 500 # потолок длины сообщения (в рунах)
history_retention: 200 # сколько сообщений хранить на клан (ring buffer; лишнее чистит крон)
send_min_interval: 2s # кулдаун между сообщениями на отправителя
send_burst_per_min: 15 # скользящий потолок сообщений в минуту на отправителя
mute_max_duration: 168h # потолок, на сколько модератор может замьютить (7d)domain.clans — параметры уровня клана; domain.clan_war — цикл войны (сиблинг domain.rivals/contest/events/stream). Скалярные длительности — Go duration-строки (Xh/Xm/Xs; дни — часами, напр. 336h = 14d, 720h = 30d), как везде в конфиге. badwords_file — путь к текстовому блоклисту имени/тега клана (одно слово/фраза на строку, # — комментарий, пустые строки игнорируются; матч case/leet-insensitive как подстрока); пустой путь или отсутствующий файл = фильтр выключен (см. config/badwords.example.txt). Запрета на вступление после выхода нет: вступить в клан можно сразу, а уход из клана во время войны закрывает участие в клановых войнах до конца текущего цикла (ограничение clans.war, см. Ограничения аккаунта). Ключ join_cooldown устарел и игнорируется; схема его принимает, чтобы старые конфиги загружались.
domain.clan_chat — базовый переиспользуемый модуль клан-чата (сиблинг clan_war). Модуль владеет своими таблицами и обращается к клановой системе только через порт membership, поэтому переносится в другие игры на этой кодовой базе без правок. Непрочитанное отдаётся в clans/check (секция chat); сообщения и модерация — под clans/chat/*. Ретеншн (history_retention) — кольцевой буфер на клан, лишнее подрезает крон clan_chat_retention. Системные события клана (вступил/вышел/исключён/смена роли/передача лидерства) публикуются в ленту как system-сообщения через общий порт эмиттера — что именно попадает в ленту, решает хост. Через тот же порт война (Forge-специфичная) шлёт в чат старт (код 100), смену фазы (код 102, meta {phase_index, type}) и завершение (код 101, meta.outcome = win/lose/tie) — по обоим кланам. Сам чат-модуль эмитит служебное «сообщение удалено» (код 900) для клиентов, уже прочитавших дальше.
Модерация чата двухуровневая. Внутри клана удалять сообщения и мьютить может co-leader+ (clans/chat/delete, clans/chat/mute). Игрок может пожаловаться на сообщение (clans/chat/report) — жалоба уходит руководству клана (clans/chat/reports), которое либо отклоняет её, либо эскалирует модератору (clans/chat/reports/escalate). Оператор в ACP видит очередь эскалированных жалоб, может посмотреть чат любого клана и удалить сообщение (acp/clans/chat/*); каждое операторское действие пишет clan_moderation_actions. Жалоба снапшотит текст и автора сообщения, поэтому переживает подрезку ретеншном.
Война — это упорядоченный список фаз (phases), каждая со своим duration. Планировщик закрывает активную фазу по истечении окна (сравнивает очки сторон → победитель фазы → начисляет winning_points, ничья — обоим → фиксирует MVP) и активирует следующую, либо финиширует войну (победитель — по сумме winning points; ничья → без победителя). Типы фаз: points (игроки репортят очки), battle (1×1 хендшейк, points_per_win за побеждённого врага, каждого — раз), results (пауза), squad5x5 (бой 5×5, squad_size бойцов). Порядок и состав меняются в phases — движок один. tier маппит счётчик tier_points клана в тир по порогам (тир замораживается на войне и задаёт размер наград). rewards.delivery выбирает доставку: mail (сервер кладёт в почту, хранится ~месяц) или claim (клиент забирает через clans/war/rewards + clans/war/rewards/claim); начисление идемпотентно (reconcile в планировщике переживает сбои). Общую награду получает только участник, набравший за войну не меньше rewards.min_points_for_reward очков (0 или не задано = 1); win-награду из них — участник победившего клана, набравший больше rewards.min_points_for_win. Состав войны на подборе не фиксируется: воюют текущие участники клана, а min_members и сила клана на подборе считаются только по тем, кто будет воевать (см. Войны кланов).
5. observability
observability:
metrics:
http_buckets_ms: [50, 100, 200, 500, 1000, 2000, 5000]
prometheus_enabled: false6. tasks
tasks.system
Системные cron-задачи, которые поднимаются из конфигурации (без записи в БД).
tasks:
system:
- name: "Reload HELM JWKS"
type: helm_certs
schedule: "0 */6 * * *"
enabled: true
start_immediately: true
scope: all # JWKS нужен на каждом узле; leader (по умолчанию) или all
- name: "Persist user data updates"
type: user_data_batch_update
schedule: "*/5 * * * *"
enabled: true
scope: leader
- name: "Dispatch alerts"
type: alert_dispatcher
schedule: "*/1 * * * *"
enabled: true
scope: leader
- name: "Clean old data"
type: data_clean
schedule: "0 2 * * *"
enabled: true
scope: leader
- name: "Stats snapshot"
type: stats_snapshot
schedule: "0 * * * *"
enabled: true
start_immediately: true
scope: leader
- name: "Order cleanup"
type: order_cleanup
schedule: "0 3 * * *"
enabled: true
scope: leader
- name: "User backup"
type: user_backup
schedule: "0 4 * * *" # ежедневно 04:00 (последние 24ч)
enabled: true
scope: leader
- name: "World backup"
type: world_backup
schedule: "30 4 * * *" # ежедневно 04:30 (последние 24ч)
enabled: true
scope: leader
- name: "Clan war matchmaker"
type: clan_war_matchmaker
schedule: "0 17 * * 1" # Пн 17:00 UTC — открыть войны нового цикла
enabled: true
start_immediately: false
scope: leader
- name: "Clan war scheduler"
type: clan_war_scheduler
schedule: "*/10 * * * *" # продвинуть фазы войны + просрочить зависшие атаки
enabled: true
start_immediately: false
scope: leader
- name: "Clan maintenance"
type: clan_maintenance
schedule: "0 3 * * *" # ежедневно: наследование лидера + автокик неактивных + GC старых войн
enabled: true
start_immediately: false
scope: leader
- name: "Clan chat retention"
type: clan_chat_retention
schedule: "0 4 * * *" # ежедневно: подрезка чата каждого клана до окна ретеншна
enabled: true
start_immediately: false
scope: leaderstats_snapshot — периодически рассчитывает агрегированную статистику пользователей (total, today, week, month, DAU, MAU) и сохраняет в in-memory StatsStore. Результат отдаётся через POST /acp/system/overview. Рекомендуется запускать раз в час на лидере; при start_immediately: true первый расчёт происходит сразу после старта.
scope управляет тем, где выполняется задача:
leader— только на активном лидере (рекомендовано для операций с единственным воркером: перенос очередей из Redis в БД и т.п.)all— на каждом сервере (подходит для задач вроде обновления сертификатов или прогрева кэшей).
Клановые cron-задачи (все scope: leader):
clan_war_matchmaker— открывает войны нового цикла, подбирая подходящие кланы и материализуя фазы из расписания (те же, что и ручнойPOST /acp/clans/war/run-matchmaking).clan_war_scheduler— продвигает фазы войн (закрывает истёкшую → активирует следующую → финиширует войну), начисляет награды завершённых войн (reconcile, идемпотентно) и просрочивает зависшие атаки. РучнойPOST /acp/clans/war/advanceдвигает только фазы, но НЕ начисляет награды и НЕ просрочивает атаки — это делает лишь этот крон.clan_maintenance— суточная уборка: наследование лидерства при простое (leader_inactive_succession), мягкий автокик неактивных участников (inactive_autokick) и retention-GC старых войн (clan_war.retention).clan_chat_retention— подрезает чат каждого клана до окнаclan_chat.history_retention(кольцевой буфер).
7. alerts
Шлюз критических уведомлений.
alerts:
destinations:
- name: payments_slack
provider: slack # slack / telegram / helm
endpoint: "https://hooks.slack.com/services/..."
severities: [critical]
tags: ["payments"]
template: payment_critical
- name: oncall_tg
provider: telegram
token: "<bot-token>"
chat_id: -1001234567
severities: [critical, warning]
template: default_telegram
- name: helm_alerts
provider: helm # пересылка событий в HELM
endpoint: "https://helm.example.com/ext/alerts/ingest"
token: "" # HELM ext.api_key — override через .env
severities: [info, warning, critical]
templates:
- id: payment_critical
provider: slack
text: |
:rotating_light: *{{ .Title }}*
{{ .Message }}
{{ if .Payload.transaction_id }}ID: `{{ .Payload.transaction_id }}`{{ end }}
- id: default_telegram
provider: telegram
text: |
{{ .Title }}
{{ .Message }}
severity={{ .Severity }}destinations— куда отправлять события (Slack webhook, Telegram-чат и т.д.), плюс фильтры по severity/tag, выбор шаблона.templates— текстовые шаблоны (Gotext/template). Доступные поля:Title,Message,Severity,Tags,Payload.
Полная JSON Schema расположена в infrastructure/config/schemas/config.schema.json.
