Пошаговый план: как спроектировать и собрать веб‑приложение для email‑кампаний — модели данных, отправка, очереди, трекинг и инструменты доставляемости.

Прежде чем выбирать стек и рисовать диаграммы, полезно зафиксировать, какую ценность даёт продукт. Не «отправлять письма», а помогать командам запускать кампании и при этом сохранять управляемость доставляемости, качества базы и аналитики.
Минимальный набор обычно включает четыре блока:
Важно заранее решить, будут ли в продукте маркетинговые рассылки, продажные последовательности (outreach) и/или транзакционные письма (чеки, сброс пароля). Для транзакционных писем критичны скорость и надёжность, для маркетинга — сегменты, контент, контроль частоты и «безопасные тормоза».
Ваше веб‑приложение отвечает за логику и данные: аудитории, сегментацию, подготовку контента, очереди задач, хранение событий и статусов, подавление (suppression) и отчёты.
SMTP/ESP‑провайдер обычно берёт на себя инфраструктуру доставки: исходящие SMTP‑соединения, репутационные механизмы, часть телеметрии, вебхуки bounces/complaints. Даже если вы отправляете через собственный SMTP, вам всё равно придётся строить процессы вокруг репутации и обратной связи.
Для MVP достаточно: кампании + списки + шаблоны + базовая статистика доставки и обработка отказов/отписок.
Расширения, которые стоит планировать заранее (в данных и событиях): A/B‑тесты, автоматизации/триггеры, прогрев домена и IP, частотные ограничения, роли и аудит действий.
Хорошие требования формулируются измеримо: стабильная отправка без «зависших» задач, прозрачный контроль доставляемости (почему письмо не дошло), воспроизводимая аналитика, а также возможность быстро остановить рассылку и защититься от случайных или вредоносных отправок.
Хорошая модель данных — это основа, на которой держатся сегментация, корректная обработка отписок и понятная аналитика. Если заложить правильные сущности и связи сразу, дальше будет проще добавлять функции без «костылей».
Аккаунт — владелец системы (компания или команда). Внутри аккаунта обычно живут несколько проектов.
Проект — отдельный продукт/бренд/направление. На уровне проекта удобно хранить настройки отправки, списки контактов и кампании.
Домен/отправитель — данные, связанные с отправкой: домен, адрес отправителя, имя, настройки подписи и технические параметры. Важно разделять «проект» и «отправителя»: один проект может отправлять от нескольких адресов, а один домен может обслуживать несколько проектов (если вы так решите).
Контакт (подписчик) — ключевая запись: email, статус (активен/отписан/в suppression), метаданные (имя, город, теги), источник появления и таймстемпы. Рекомендуется хранить нормализованный email (lowercase, без пробелов) и отдельно «сырой» ввод, чтобы проще находить проблемы импорта.
Сегмент — сохранённое правило отбора контактов (например, «из Москвы и с тегом webinar»). Сегмент лучше хранить как условие (фильтр), а не как статический список: так он всегда актуален.
Кампания — это контейнер: тема, контент письма, выбранный отправитель, аудитория (сегмент/список), расписание.
Минимальный набор статусов:
Дополнительно полезны «Пауза» и «Отменена», но даже базовых четырёх достаточно, чтобы корректно строить отчёты и не допускать двойных запусков.
События — это «лента фактов» по доставке и реакции получателя. Обычно выделяют:
Практичный подход: хранить события отдельно от контакта и кампании (таблица/коллекция events), с полями contact_id, campaign_id, timestamp, type, payload (причина bounce, URL клика, user-agent и т. п.). Это помогает строить аналитику без постоянного изменения схемы.
Согласие — не просто галочка. Храните:
Так вы сможете быстрее отвечать на вопросы поддержки и разбирать спорные ситуации по жалобам.
Если в продукте есть «списки», лучше моделировать связь контакт ↔ список как many-to-many, чтобы один email мог быть в нескольких списках.
Отдельно держите suppression list — глобальный реестр адресов, на которые нельзя отправлять (отписки, жалобы, жёсткие bounces). Этот слой должен перекрывать любые сегменты и списки, иначе вы рискуете повторными жалобами и падением доставляемости.
Если рассылками занимается больше одного человека, система доступов быстро становится не «опцией», а основой порядка. Ошибка в правах может стоить и репутации отправителя, и данных подписчиков.
Начните с простых и понятных ролей, которые совпадают с реальными обязанностями:
Даже при таком наборе стоит добавить точечные разрешения: «может импортировать базу», «может удалять подписчиков», «может запускать отправку». Это снижает риск случайных действий.
Если у вас несколько клиентов или брендов, делайте multi‑tenant модель: каждый объект (подписчик, кампания, событие, шаблон) принадлежит конкретному проекту (tenant).
Практическое правило:
project_id;Это предотвращает ситуации, когда сотрудник одного проекта случайно видит базу другого.
Для внешних систем нужны API‑ключи с ограниченными правами и сроками жизни (или возможностью ротации). Хорошая практика — отдельный ключ на интеграцию, чтобы можно было быстро отключить конкретный источник.
Для webhooks добавьте подпись запросов (shared secret), защиту от повторов и понятный экран «история доставок вебхуков».
Лимиты помогают не только экономике, но и безопасности: например, квота на отправку в сутки, ограничение скорости, лимит на импорт. В интерфейсе формулируйте это честно: лимиты снижают риск злоупотреблений, но не являются обещанием «гарантированной доставляемости».
Аудит‑лог должен отвечать на вопросы «кто импортировал базу», «кто изменил сегмент», «кто запустил кампанию», «какой шаблон отправили». Храните: пользователя, время, действие, объект и минимальный контекст (например, размер импорта). Это упрощает расследование инцидентов и внутренний контроль.
Хороший редактор — это место, где маркетинг и инженерия перестают спорить. Он должен позволять быстро собрать письмо, не ломая вёрстку, и при этом оставаться предсказуемым для отправки и аналитики.
Практичный вариант — поддержать оба сценария:
В обоих режимах заложите переменные персонализации, например {{first_name}}, {{city}}. Важно, чтобы редактор показывал «примерные» значения в предпросмотре, а система — подставляла реальные значения только при генерации письма.
Шаблон почти никогда не бывает «готов навсегда». Поэтому храните версии: кто и когда изменил, комментарий к правке, возможность отката. Удобно иметь статусную модель: черновик → на ревью → опубликован.
Предпросмотр лучше сделать в нескольких ширинах (например, 320/600/800 px) и в светлой/тёмной теме. Это помогает увидеть проблемы с переносами, кнопками и мелким текстом до отправки.
Добавьте тестовую отправку на список адресов (с ограничениями по безопасности) и автоматические проверки:
Если письмо мультиязычное, заведите локали как вариации одного шаблона (RU/EN и т. д.) и дайте возможность выбирать таймзону для дат и «сегодня/завтра» в тексте.
Изображения и файлы храните как ассеты с контролем доступа: генерируйте безопасные URL, ограничивайте типы/размеры, а при подстановке переменных экранируйте значения, чтобы пользовательские данные не могли «сломать» HTML.
Сегментация — это способ отправлять меньше писем, но точнее: нужным людям, в нужный момент, с понятной причиной попадания в аудиторию. Если сделать её слишком «умной», команда перестанет ей пользоваться; если слишком простой — вы потеряете доставляемость и конверсии.
Начните с набора фильтров, которые можно комбинировать без «магии»:
Важно, чтобы каждый фильтр объяснялся человеческим языком и имел предпросмотр: «в сегмент попадёт 12 430 контактов».
Сделайте два типа аудиторий:
В интерфейсе полезно показывать, что именно изменит состав: «добавится/убавится» после пересчёта.
Персонализацию держите на двух источниках: атрибуты профиля (имя, город, тариф) и события (последний просмотренный товар, категория кликов). В шаблонах — простые плейсхолдеры с безопасными дефолтами: если имени нет, подставьте нейтральное обращение.
Сегментация всегда должна учитывать глобальные исключения: отписки, hard bounce, жалобы — такие контакты нельзя «вернуть» сегментом.
Дополнительно введите метки качества: «подозрительный» (ролевая почта вроде info@, временный домен, частые soft bounce, резкая смена IP/страны по событиям). Такие контакты лучше отправлять в отдельный карантинный сегмент и не включать в массовые рассылки по умолчанию.
Пайплайн отправки — это «конвейер», который превращает кампанию и список получателей в фактически доставленные письма. Его задача — быть предсказуемым: не слать лишнее, не терять письма при сбоях и держать скорость под контролем.
Для большинства продуктов разумнее начинать с SMTP‑провайдера или ESP (email service provider): вы получаете готовую репутацию IP (или понятный путь к выделенному IP), управление скоростями, обратные события (bounces/complaints) и меньше операционных рисков.
Свой MTA имеет смысл, когда есть сильная экспертиза и особые требования: очень большие объёмы, необходимость полностью контролировать маршрутизацию, собственные правила повторов/очередей, интеграции с несколькими каналами доставки. Но цена ошибки здесь высокая: ухудшение доставляемости и блокировки.
Удобно разложить отправку на независимые задачи (workers) и связать их очередями:
Подготовка: фиксируем «снимок» кампании (тема, контент, настройки), получаем список получателей.
Рендер: подставляем персонализацию, собираем MIME (HTML+текст), добавляем заголовки.
Отправка: обращаемся к SMTP/ESP, учитываем лимиты скорости.
Ретраи: временные ошибки отправляем на повтор; постоянные — фиксируем и прекращаем попытки.
Так вы сможете масштабировать «узкие места» отдельно: например, рендер бывает тяжелее, чем отправка.
Планировщик должен уметь:
Практика: хранить «желаемое локальное время + TZ контакта» и вычислять фактическое UTC‑время постановки в очередь.
Главный принцип: на уровне данных должно быть невозможно случайно отправить одно и то же письмо одному и тому же контакту дважды.
Обычно вводят ключ вроде campaign_id + contact_id + message_variant + scheduled_at и уникальный индекс. Если worker упал после отправки, но до записи статуса, повторная попытка упрётся в уникальность и не продублирует письмо.
Отправляйте не «всех сразу», а батчами (например, по 500–2000 получателей) с контролем rate limit (писем/сек) и параллелизма. На каждую попытку фиксируйте статус и причину ошибки.
Для ретраев используйте экспоненциальную задержку (например, 1 мин → 5 мин → 30 мин) и «потолок» попыток. Временные ошибки (таймауты, 4xx) — повторяем, постоянные (5xx/жёсткие отказы провайдера) — прекращаем и помечаем.
Если хотите углубиться в практику списков исключений и статусов, логично связать этот раздел с /blog/bounces-complaints-unsubscribes.
Доставляемость — это не «магия почтовиков», а предсказуемый результат: домен идентифицирован, письма подписаны, трафик ведёт себя аккуратно, а база чистая. Если эти части не настроены, даже хороший контент может стабильно попадать в спам или не доходить вовсе.
SPF говорит получателю, какие серверы имеют право отправлять письма от вашего домена. DKIM добавляет криптоподпись к каждому письму, подтверждая, что оно не было изменено по дороге. DMARC задаёт политику: что делать, если SPF/DKIM не сошлись, и куда слать отчёты.
В приложении стоит заложить простой чек‑лист проверки:
Важно: проверяйте не только наличие записей, но и типичные ошибки — лишние пробелы, неверный хост, несколько SPF‑записей сразу.
Поле From должно быть стабильным и узнаваемым (один домен, единый стиль имён), Reply‑To — туда, где действительно ответят. Return‑Path (технический адрес для возвратов) лучше выделять отдельно и привязывать к обработчику bounces. Тогда ваше приложение сможет автоматически связывать возврат с конкретной отправкой и быстрее принимать решения по исключениям.
Если вы начинаете с нового домена или IP, не отправляйте сразу «всем». Прогрев — это плавное увеличение объёма на самых вовлечённых сегментах.
В продукте это удобно реализовать как режим «прогрева»: лимиты в сутки/час, приоритет активных подписчиков, и обязательный мониторинг метрик (доля отказов, жалобы, просадки по доставке).
Письмо должно быть валидным: аккуратный HTML, корректные ссылки, понятная тема, заметный текстовый блок. Старайтесь держать баланс текста и изображений и не прятать ключевую информацию в одной картинке.
Хорошая практика для приложения — встроенная проверка перед отправкой: валидатор ссылок, предупреждения о «тяжёлых» изображениях и предпросмотр в тёмной/светлой теме.
Доставляемость легко испортить, если продолжать писать на адреса с постоянными ошибками. Нужна suppression‑политика: жёстко исключать hard bounce, аккуратно ограничивать повторы по soft bounce и мгновенно уважать отписки/жалобы.
Чем быстрее приложение переводит проблемные адреса в исключения, тем стабильнее репутация домена и тем меньше «шум» в статистике отправок.
Ошибки доставки, жалобы на спам и отписки — это не «неприятные мелочи», а сигналы, от которых напрямую зависит доставляемость и репутация домена/отправителя. Поэтому их лучше обрабатывать как входящие события с чёткими правилами, а не как строки в отчёте.
Минимальный набор входящих событий:
У разных провайдеров они называются по‑разному, поэтому полезно иметь слой «нормализации»: привести все статусы к единым типам, сохранить исходный код/описание и источник события.
Предпочтительный способ — webhooks от провайдера отправки: они приходят быстро, содержат идентификаторы сообщения и позволяют надёжно связать событие с конкретной отправкой.
Если провайдер не даёт вебхуки (редко, но бывает), можно поддержать парсинг почтового ящика для возвратов (bounce mailbox). Это менее надёжно: письма могут быть в разных форматах, иногда теряются связи с конкретной кампанией, сложнее поддержка.
Сделайте единый suppression list на проект/аккаунт, который проверяется перед любой отправкой (кампания, триггер, транзакционные письма — по вашему правилу). Важно хранить:
В аналитике показывайте факты, а не предположения: количество hard/soft bounce, топ кодов ошибок, долю жалоб, динамику по времени. Если причина не распознана однозначно — так и отмечайте («unknown/other») и храните оригинальное сообщение/код провайдера, чтобы можно было улучшить классификацию позже.
Аналитика в email‑кампаниях нужна не «ради цифр», а чтобы понимать, что реально происходит: письма доставляются, аудитория реагирует, а изменения в контенте дают эффект. Важно сразу договориться о честных определениях метрик — иначе отчёты будут красивыми, но бесполезными.
Open — метрика с ограниченной точностью. Многие почтовые клиенты кешируют изображения, блокируют их по умолчанию или «предзагружают» контент, из‑за чего открытия могут как недосчитываться, так и переоцениваться. Поэтому open лучше воспринимать как ориентир для сравнений (A/B, сегменты, темы), а не как абсолют.
Click обычно надёжнее, потому что требует действия пользователя. Но и здесь бывают искажения: корпоративные сканеры безопасности могут «кликать» по ссылкам автоматически.
Классический подход — заменять ссылки в письме на трекинговые редиректы, например:
/r/{campaign_id}/{subscriber_id}/{link_id}?sig=...
Редирект логирует событие click и отправляет пользователя на исходный URL.
Чтобы защищаться от подмены параметров:
sig),link_id действительно принадлежит кампании.UTM‑метки добавляйте автоматически по правилам (например, только для внешних ссылок), чтобы конверсии проще стыковались с веб‑аналитикой.
Open обычно реализуют через 1×1 pixel (изображение) с уникальным URL. Сделайте эту функцию включаемой на уровне кампании и честно объясняйте в интерфейсе, что часть клиентов почты может скрывать или искажать открытия.
Для скорости интерфейса храните агрегаты (счётчики по кампании/дню/ссылке) и отдельно — сырые события при необходимости (аудит, разбор инцидентов, антифрод). Часто достаточно хранить сырые логи ограниченное время, а агрегаты — дольше.
Базовый дашборд кампании: отправлено/доставлено, bounces, жалобы, отписки, уникальные клики и топ‑ссылки. Отдельно полезен «фильтр по сегментам», чтобы быстро сравнить реакцию разных аудиторий и найти, где падает доставляемость писем.
Качество базы и корректные согласия — основа для нормальной доставляемости и спокойной работы с жалобами. Поэтому импорт и сбор подписок стоит продумать так же тщательно, как пайплайн отправки.
Импорт CSV лучше делать мастером в несколько шагов: загрузка → сопоставление колонок → проверка → подтверждение. На этапе проверки показывайте пользователю «предпросмотр проблем»: некорректные email, пустые обязательные поля, странные даты.
Дедупликацию обычно делают по нормализованному email (trim, lower-case). Если файл содержит повторяющиеся строки, важно выбрать поведение: «оставить последнюю», «объединить непустые поля», «пропустить дубликаты». Отдельно полезно ловить «ролевые» адреса (info@, sales@) и решать, импортировать ли их.
Если подписка создаётся через API, добавьте базовую защиту: rate limit на IP/ключ, невидимое поле‑ловушку (honeypot) для ботов, проверку домена/формата адреса и логирование попыток. Для публичных форм полезна очередь на обработку, чтобы пики трафика не «роняли» приложение.
Double opt‑in стоит сделать опцией на уровне списка/формы: кому‑то важнее скорость, кому‑то — максимальная чистота базы. В сценарии подтверждения храните токен, срок жизни, статус и событие «подтверждено».
Храните не только факт подписки, но и контекст: источник (форма, импорт, API), дата/время, версия текста согласия (или ссылка), а также IP и user-agent — если это требуется вашими правилами и юристами. Это помогает разбирать спорные жалобы и запросы поддержки.
Сделайте понятные операции: экспорт данных подписчика (в машиночитаемом виде) и удаление/анонимизация. Важно разделять «удалить персональные данные» и «оставить запись в suppression list», чтобы случайно не начать снова отправлять письма человеку, который попросил удалить данные или отписался.
Даже идеально спроектированный сервис рассылок ломается на мелочах: некорректный рендер шаблона, «уплывшая» сегментация, пик очереди после импорта или внезапный всплеск ошибок в вебхуках. Поэтому качество здесь — это не один вид тестов, а связка: проверяем логику, выдерживаем нагрузку, видим аномалии и безопасно выкатываем изменения.
Начните с юнит‑тестов там, где баги дороже всего:
Полезная практика — «золотые снапшоты» для HTML: фиксируете ожидаемый фрагмент письма и сравниваете при изменениях редактора/шаблонизатора.
Отдельно прогоняйте сценарии «как в жизни»: batch‑отправка на десятки тысяч получателей, пики после планировщика, искусственные ошибки SMTP/API для проверки ретраев и дедупликации задач. Важно измерять не только скорость, но и рост очередей, время ожидания, долю повторных попыток.
Минимальный набор наблюдаемости: метрики (время обработки, скорость отправки, размер очередей), структурированные логи, трассировка фоновых задач (включая цепочку «планировщик → очередь → воркер → провайдер»).
Настройте алёрты на:
Держите отдельные окружения (dev/stage/prod), выполняйте миграции БД атомарно и предсказуемо (в идеале — назад‑совместимые изменения), а секреты храните в безопасном хранилище, а не в переменных «на сервере». Для релизов фоновых воркеров продумайте «мягкую остановку», чтобы не терять задачи при выкатывании.
Даже идеально собранный пайплайн отправки не спасёт, если продукт позволяет нарушать правила, провоцирует жалобы или хранит данные небезопасно. Эта часть — про «гигиену» сервиса: что обязательно встроить в интерфейс и процессы, чтобы рассылки оставались легальными, прозрачными и управляемыми.
Отписка в один клик. В каждом письме должна быть понятная ссылка на отписку, а действие — применяться быстро (лучше сразу) и фиксироваться событием.
Понятный отправитель: корректные From/Reply‑To, человеческое имя бренда, рабочий адрес для обратной связи.
Прозрачность: объясняйте, почему человек получает письмо (например, «вы подписались на…»), и храните источник согласия (форма, дата, IP/UTM при необходимости).
Управление частотой: дайте пользователю сервиса инструменты, чтобы не «засыпать» подписчиков (лимиты, quiet hours, частотные правила по сегментам).
Разделите роли (владелец, маркетолог, аналитик, саппорт) и ограничьте опасные действия: импорт, запуск кампании, редактирование доменов и webhooks.
Секреты (SMTP‑ключи, API‑токены, приватные DKIM‑ключи) храните только в зашифрованном виде и с принципом минимальных прав. Логи не должны случайно содержать тела писем, токены и персональные данные — особенно в ошибках и трассировках.
Снизьте риск ошибок интерфейсом: мастер подключения домена, подсказки по согласию, «pre‑flight» проверка кампании (отписка, From, сегмент, тест‑отправка), шаблоны политик и FAQ для саппорта.
Если задача — не только спроектировать архитектуру «на бумаге», но и быстро проверить гипотезы (интерфейс кампаний, редактор шаблонов, очереди задач, базовые роли), полезно опираться на подход vibe‑coding.
Например, в TakProsto.AI можно собрать рабочий прототип веб‑приложения через чат: описываете сущности (проекты, контакты, кампании, события), экраны и правила (suppression, ретраи, лимиты), а платформа помогает сгенерировать приложение на React, бэкенд на Go и PostgreSQL, а также подготовить деплой. Удобно, что есть planning mode (чтобы сначала согласовать модель данных и поток событий), снапшоты и откат, экспорт исходников и хостинг в РФ — это особенно важно, когда вы строите систему с чувствительными данными подписчиков.
А дальше вы уже решаете, что «докрутить» вручную: интеграции с конкретным SMTP/ESP, нормализацию вебхуков, детали планировщика и наблюдаемость.
Если вы готовите коммерческую версию и тарифы — логично продолжить с /pricing.
А чтобы укрепить доставляемость и пройти базовую верификацию домена, вынесите подробный гайд в /blog (например, отдельная статья про SPF/DKIM/DMARC с примерами записей и типовыми ошибками).
Начните с ядра, которое даёт ценность без «магии»:
Всё остальное (A/B, автоматизации, прогрев, частотные правила) лучше заложить в события и модель данных, но не обязательно выпускать в первой версии.
Разделяйте ответственность:
Даже при своём SMTP вам всё равно понадобится сбор обратной связи и строгие правила исключений.
Минимальный набор сущностей, который масштабируется:
Suppression — это глобальный «запрет на отправку», который перекрывает любые сегменты и списки.
Практика:
Это напрямую защищает доставляемость и снижает риск жалоб.
Держитесь принципа «минимально нужных прав»:
project_id;Это помогает предотвращать «случайные отправки» и разбирать инциденты.
Обычно выигрывает гибрид:
Обязательно:
{{...}} с безопасными дефолтами;Сегментацию лучше делать «объяснимой»:
И всегда применяйте исключения: отписки, жалобы, hard bounce должны блокировать отправку независимо от условий сегмента.
Разложите отправку на этапы и очереди:
Ключевое — идемпотентность: уникальный ключ вроде campaign_id + contact_id + variant + scheduled_at, чтобы не продублировать письмо при сбоях воркера.
Встроите чек‑лист в продукт:
Плюс режим «прогрева» для новых доменов/IP: постепенный рост объёма и мониторинг отказов/жалоб.
Сделайте обработку событий строгой и быстрой:
События лучше нормализовать в единые типы и сохранять «сырой» код/описание провайдера для диагностики.
contact_id, campaign_id, type, timestamp, payload.Так аналитика и сегментация не будут ломаться при добавлении новых событий.