White-label SaaS-бот — это когда вы один раз построили платформу, а потом продаёте её десяткам/сотням клиентов под их брендом, цветами, доменами и логикой. На платформе MAX от VK Tech это особенно интересный сегмент: 2026 год — взрывной рост ботов в РФ, ниши не насыщены, и студия с одним правильным SaaS-продуктом может за год набрать 50–200 клиентов с MRR 0.5–5 млн ₽. В этой статье — как спроектировать white-label SaaS-бот для MAX: мультитенантная архитектура, изоляция данных, биллинг, кастомизация без правки кода, deployment-модели (shared / dedicated / on-prem) и финансовая модель.
Что такое white-label SaaS-бот
| Свойство | Что значит |
|---|---|
| White-label | Клиент использует бот под своим брендом, без упоминания вас |
| SaaS | Подписочная модель, без покупки лицензии |
| Multi-tenant | Один код, несколько изолированных тенантов |
| Self-service | Клиент сам подключает свой токен бота, настраивает контент |
| Биллинг | Автоматическое выставление счетов и блокировка при неоплате |
Примеры ниш: бот для записи (салоны, клиники, барбершопы), бот-магазин для малого бизнеса, бот-каталог для производителей мебели, HR-бот для подбора курьеров, бот для микрорассылок.
Уровни мультитенантности
Три модели:
- Shared everything — один Postgres, общие таблицы с полем
tenant_id. Дёшево, масштабируется хорошо, риск утечки между тенантами при ошибке в коде. - Shared schema, separate row-level security — RLS на уровне Postgres, query автоматически фильтруется по tenant. Безопаснее.
- Schema-per-tenant — отдельная Postgres-схема на каждого. Изоляция полная, но миграции — на каждой схеме.
- DB-per-tenant — отдельная база. Полная изоляция, но дорого и сложно масштабировать.
Для SaaS-бота на старте — shared schema + tenant_id + RLS. Для крупных корпоративных клиентов с особыми требованиями — schema-per-tenant.
Модель данных
CREATE TABLE tenants (
id BIGSERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "salon-vesna"
title TEXT NOT NULL,
bot_token TEXT, -- токен MAX-бота клиента
plan TEXT DEFAULT 'free',
status TEXT DEFAULT 'active', -- active|suspended|trial
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
settings JSONB DEFAULT '{}' -- бренд, цвета, тексты
);
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
messenger_user_id BIGINT NOT NULL,
first_name TEXT,
UNIQUE (tenant_id, messenger_user_id)
);
CREATE INDEX ix_users_tenant ON users(tenant_id);
-- Для каждой бизнес-таблицы — tenant_id
CREATE TABLE appointments (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
user_id BIGINT REFERENCES users(id),
-- ...
);
ALTER TABLE appointments ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON appointments
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::bigint);
В коде в начале каждого запроса:
async def with_tenant(conn, tenant_id: int):
await conn.execute(f"SET LOCAL app.tenant_id = {tenant_id}")
После этого все запросы автоматически фильтруются — никакой риск SELECT * без tenant_id не приведёт к утечке.
Роутинг сообщений на правильный tenant
Один процесс может обслуживать сразу всех тенантов. Идея — у каждого клиента свой токен MAX-бота, но webhook у всех — один общий вида /webhook/<tenant_slug>. Slug извлекаем из URL:
@app.post("/webhook/{tenant_slug}")
async def on_webhook(tenant_slug: str, request: Request, x_secret: str | None = Header(None)):
tenant = await get_tenant_by_slug(tenant_slug)
if not tenant or tenant.status != "active":
raise HTTPException(404)
if not hmac.compare_digest(tenant.webhook_secret, x_secret or ""):
raise HTTPException(401)
update = await request.json()
async with db.acquire() as conn:
await with_tenant(conn, tenant.id)
await dispatch(tenant, update, conn)
return {"ok": True}
В MAX-кабинете каждого клиента webhook регистрируется на свой URL https://saas.example.ru/webhook/salon-vesna.
Кабинет клиента
Веб-приложение, где клиент:
- подключает токен своего MAX-бота;
- настраивает бренд, цвета, тексты;
- управляет содержимым (услуги, расписание, прайс);
- видит метрики и историю;
- управляет подпиской и платежами.
Авторизация по email + password или SSO. Доступ к данным — только своего тенанта (через RLS).
Onboarding нового клиента
1. Регистрация в кабинете → email/телефон → trial 14 дней.
2. Создаётся tenant, slug, webhook_secret.
3. Клиент создаёт бота в MAX (через @MasterBot или аналог), копирует токен.
4. Вставляет токен в кабинет.
5. Платформа делает setWebhook на https://saas.example.ru/webhook/{slug}.
6. Клиент заполняет начальный контент (услуги/прайс) или импортирует CSV.
7. Готово, бот работает под брендом клиента.
Цель — первый запуск работающего бота за 5–10 минут без участия техподдержки.
Кастомизация без правки кода
Бренд, цвета, тексты — JSONB в tenants.settings:
{
"brand": {
"name": "Salon Vesna",
"logo_url": "https://cdn.../logo.png",
"color": "#5B8DFF"
},
"messages": {
"greeting": "Здравствуйте! Это бот {brand_name}.",
"booking_confirmed": "✅ Запись на {date} подтверждена",
"reminder_24h": "Напоминаем, завтра в {time} вы записаны на {service}"
},
"features": {
"loyalty_program": true,
"online_payment": false,
"reviews": true
}
}
В коде:
def t(tenant: Tenant, key: str, **kw) -> str:
template = tenant.settings.get("messages", {}).get(key, DEFAULT_MESSAGES[key])
return template.format(brand_name=tenant.settings["brand"]["name"], **kw)
await bot.send_message(chat_id, t(tenant, "booking_confirmed", date="15 марта", time="14:00", service="стрижка"))
Для глубокой кастомизации (новые типы услуг, нестандартные сценарии) — feature flags + редактируемые шаблоны.
Тарифы и биллинг
Типичный SaaS-pricing для бот-платформы:
| Тариф | Цена/мес | Ограничения |
|---|---|---|
| Free / Trial | 0 | 14 дней, до 100 пользователей |
| Start | 1 990 ₽ | до 1 000 активных, базовые функции |
| Pro | 4 990 ₽ | до 10 000, доп. функции, рассылки |
| Business | 14 990 ₽ | до 50 000, white-label, API |
| Enterprise | от 50 000 ₽ | безлимит, dedicated, SLA, кастомизация |
Биллинг — через ЮKassa / Тинькофф Эквайринг, рекуррентные платежи. При неоплате — суспенд с graceful degrade:
- день 1 — push с напоминанием;
- день 3 — баннер в кабинете;
- день 7 — отключение исходящих сообщений;
- день 30 — полный suspend, данные сохраняются 90 дней.
async def cron_billing():
overdue = await db.fetch_all("""
SELECT * FROM tenants
WHERE status='active'
AND next_billing_at < now() - interval '7 days'
""")
for t in overdue:
await suspend(t.id)
await notify_owner(t.id, "Подписка приостановлена. Оплатите для возобновления работы.")
Лимиты и квоты
Каждый тариф — свои лимиты. Реализация на уровне rate limit:
TIER_LIMITS = {
"start": {"users": 1000, "broadcasts_per_month": 4},
"pro": {"users": 10000, "broadcasts_per_month": 20},
"business": {"users": 50000, "broadcasts_per_month": 100},
}
async def check_users_limit(tenant: Tenant) -> bool:
cur = await db.fetch_val("SELECT count(*) FROM users WHERE tenant_id = $1", tenant.id)
limit = TIER_LIMITS[tenant.plan]["users"]
return cur < limit
Превышение → push «Лимит превышен, обновите тариф» + блокировка роста.
Deployment-модели
| Модель | Кому | Стоимость |
|---|---|---|
| Shared SaaS | малый бизнес | минимум, окупается с 50+ клиентов |
| Dedicated SaaS | средний бизнес | отдельный namespace в k8s, +50–100% |
| On-prem | enterprise, банки | разовый платёж + поддержка |
On-prem имеет смысл при ARR 1.5+ млн ₽/клиент или жёстких требованиях по информационной безопасности.
Изоляция инцидентов
Один тенант не должен валить других. Меры:
- rate limit на уровне tenant_id (отдельный bucket для каждого);
- лимит ресурсов в k8s по namespace (если Dedicated);
- circuit breaker на исходящие API (если у одного клиента сломалась интеграция CRM);
- алерты в Prometheus с label
tenant_idдля мониторинга «кто шумит».
Финансовая модель
| Параметр | Значение |
|---|---|
| Средний чек (ARPU) | 4 000 ₽/мес |
| Количество клиентов на 12 мес | 100–200 |
| MRR | 400 тыс. – 800 тыс. ₽ |
| Churn в месяц | 3–7% |
| LTV | ARPU / churn = 13–33 ARPU = 50–130 тыс. ₽ |
| CAC | 5–15 тыс. ₽ |
| LTV/CAC | 5–15 (хорошая модель) |
Стоимость инфры на 100 клиентов: 30–80 тыс. ₽/мес (k8s 50–100K MAU суммарно). Маржа в зрелом состоянии — 60–80%.
Common pitfalls
- Один общий webhook без HMAC по тенанту — атакующий шлёт обновления чужого бота.
- Шеринг данных через JOIN без
tenant_id— катастрофическая утечка. - Один Redis для rate limit без префикса
tenant_id— клиент A blocks клиента B. - Миграции без проверки на больших тенантах — на 1000 строках работает, на 1М — лежит.
- Бесплатный trial без CC — масс-регистрация ботов и спам.
- «Кастомизация» через прямой доступ к БД — невозможно мигрировать, обновлять.
Итого
White-label SaaS-бот на MAX строится по схеме: shared schema с tenant_id и RLS (или schema-per-tenant для крупняка), один процесс обслуживает всех тенантов, webhook вида /webhook/{slug} с HMAC по секрету тенанта, кабинет клиента с self-service onboarding (5–10 минут до запуска), кастомизация через JSONB settings (бренд, тексты, feature flags), биллинг через ЮKassa с рекуррентами и graceful degrade при неоплате, лимиты по тарифу, изоляция через per-tenant rate limit и Prometheus labels. Финансовая модель: ARPU 4 тыс. ₽, 100–200 клиентов за год = MRR 0.4–0.8 млн ₽, маржа 60–80% при инфре 30–80 тыс. ₽/мес. Срок MVP — 8–14 недель и 3–5 млн ₽; время до окупаемости — 6–14 месяцев в зависимости от ниши и каналов привлечения.
Частые вопросы
Какую модель мультитенантности выбрать?
Для большинства SaaS-ботов на старте — shared schema с tenant_id во всех таблицах + Row Level Security в Postgres. Это даёт хорошую изоляцию (RLS защищает от случайных утечек при ошибке в SQL), легко масштабируется и не требует миграций на каждой схеме. Schema-per-tenant имеет смысл для enterprise-клиентов с особыми требованиями. DB-per-tenant — только для очень крупных или регулируемых индустрий (банки, медицина), потому что управлять сотней баз данных дорого и сложно.
Как роутить webhook MAX на правильного тенанта?
У каждого клиента свой токен бота, но webhook у всех на общем сервере вида https://saas.example.ru/webhook/\{tenant_slug\}. При регистрации бота в MAX вы вызываете setWebhook с этим URL и уникальным secret_token для тенанта. На входящем запросе извлекаете slug из URL, находите тенант, проверяете подпись secret_token (hmac.compare_digest), устанавливаете SET LOCAL app.tenant_id и обрабатываете update. Это позволяет одному процессу обслуживать сотни тенантов без отдельных деплоев.
Как кастомизировать бота под бренд клиента без правки кода?
Все кастомизируемые элементы (название, цвета, тексты, feature flags) храните в tenants.settings JSONB. Шаблоны сообщений с placeholder'ами вида {brand_name}, {service}, {time}. Клиент в кабинете редактирует тексты через UI, бот подставляет их при отправке. Для продвинутой кастомизации — feature flags ({features: {loyalty: true, payments: false}}) включают/отключают модули. Глубокую кастомизацию (новые типы сценариев) делайте через расширения на уровне платформы, а не правкой кода для одного клиента.
Как настроить биллинг и блокировку при неоплате?
Рекуррентные платежи через ЮKassa или Тинькофф Эквайринг (auto-payment с привязанной карты). Cron раз в день проверяет next_billing_at, пытается списать, при успехе — продлевает; при неудаче — последовательность graceful degrade: день 1 push, день 3 баннер в кабинете, день 7 блокировка исходящих сообщений (бот всё ещё принимает, но не отвечает), день 30 полный suspend, данные сохраняются 90 дней. Это даёт клиенту шанс восстановить подписку без потери данных и снижает churn от технических причин.
Какие тарифы установить для SaaS-бота?
Типичная сетка: Free trial 14 дней, Start 1 990 ₽ (до 1K пользователей, базовые функции), Pro 4 990 ₽ (до 10K, расширенные функции и рассылки), Business 14 990 ₽ (до 50K, white-label, API), Enterprise от 50 000 ₽ (безлимит, dedicated, SLA, кастомизация). Конкретные числа зависят от ниши: для малого бизнеса (запись в салоны) median 2–4 тыс. ₽; для среднего (онлайн-курсы, маркетплейсы) — 5–15 тыс. ₽. Гайдлайн: ARPU должен быть в 5–15 раз выше CAC, иначе unit-экономика плохая.
Как защитить тенантов друг от друга?
Несколько слоёв: RLS в Postgres гарантирует, что SQL не может прочесть чужие данные даже при ошибке в коде; rate limit в Redis с префиксом tenant_id (один шумный клиент не блокирует других); circuit breaker на исходящие API (поломанная интеграция CRM одного клиента не валит весь сервис); ресурсные лимиты в k8s по namespace для Dedicated; алерты в Prometheus с label tenant_id для быстрого выявления «кто шумит». Регулярные пентесты на multi-tenant escape — обязательно при наличии корпоративных клиентов.
Сколько занимает разработка и какова окупаемость?
MVP white-label SaaS-бота с одним сценарием (например, «бот для записи в салоны»), кабинетом клиента, биллингом, кастомизацией бренда — 8–14 недель и 3–5 млн ₽. Полная платформа с 2–3 сценариями, расширенными настройками, мобильным кабинетом, API, маркетплейсом дополнений — 6–9 месяцев и 8–15 млн ₽. Точка безубыточности при ARPU 4 тыс. ₽ и инфраструктуре 50 тыс. ₽/мес — около 30 платных клиентов. Реалистичная цель за год — 100–200 клиентов и MRR 0.4–0.8 млн ₽. Окупаемость инвестиций — 12–24 месяца при сильном продакт-маркет-фите.