E7 Entertainment проводит живые мероприятия в Центральной Европе — концерты, корпоративы, театр. Когда нашли меня, они продавали билеты через платформу-реселлера, которая забирала 15% с билета, не давала данных о покупателях, и мобильный чекаут был настолько сломан, что 40% покупателей бросали на экране выбора мест.
Они хотели свою платформу. Я собрал её за 12 недель.
Проблема в цифрах
Три болевых точки, каждая стоит реальных денег:
15% комиссия реселлера. На 1,000-местном мероприятии по €35/билет — это €5,250 уходит до начала первого акта. За год мероприятий реселлер был вторым по величине расходом после аренды площадки.
Нет данных о клиентах. Билеты, проданные через реселлера, принадлежали реселлеру. E7 не могли написать предыдущим покупателям о новых мероприятиях, не видели, на каких событиях повторные посетители, не могли делать маркетинг, кроме платной рекламы.
40% брошенных мобильных чекаутов. Мобильный выбор мест реселлера — десктопный интерфейс, запихнутый в экран телефона. Pinch-to-zoom на SVG-карте зала, потом попробуй тапнуть по кружку в 12px. Воронка: просмотр → выбор мест → данные → оплата. 40% уходили между шагами 1 и 2 на мобилке. Это не UX-проблема — это утечка выручки.
Ограничения
12 недель, жёсткий дедлайн. У E7 самое крупное мероприятие сезона через 14 недель. Нам нужно 12 на разработку и 2 на тестирование с реальными событиями на площадках поменьше.
Должен выдерживать 12K билетов за событие. Крупнейший зал — 3,000 мест. Четыре мероприятия в месяц. Система должна обрабатывать одновременный выбор мест без перепродажи — если два человека смотрят на одно место, только один получает его.
Только Stripe. E7 уже использовал Stripe для продажи мерча. Без нового платёжного провайдера. Про граничные случаи Stripe-вебхуков написал отдельно — часть уроков пришла прямо из этого проекта.
Что отгрузили
Стек
Фронтенд: Next.js 15, React 19, TypeScript, Tailwind CSS. Публичный сайт (список мероприятий, выбор мест, чекаут) и админ-панель (управление событиями, аналитика, сканер) — одно и то же Next.js-приложение с маршрутизацией по ролям.
Бэкенд: Node.js с Prisma ORM на PostgreSQL. Redis для состояния сессий, блокировок мест и rate limiting. API-слой — это API-маршруты Next.js, без отдельного бэкенд-сервиса.
Платежи: Stripe Checkout для потока покупки. Вебхуки для фулфилмента (выпуск билетов, отправка QR-кодов, обновление инвентаря). Stripe Connect готов для будущих мультивендорных событий.
Реальное время: WebSocket через лёгкий Node-сервер. Обновления карты мест транслируются всем подключённым клиентам за <100ms. Когда кто-то выбирает место, все остальные видят, что оно стало серым, мгновенно.
Инфраструктура: Vercel для Next.js-приложения. Supabase для PostgreSQL + реальное время. Cloudflare R2 для PDF-билетов и QR-изображений. Sentry для трекинга ошибок.
Выбор мест — сложная часть
Интерактивная карта зала — ядро продукта и самая сложная инженерная задача.
У каждой площадки есть SVG-карта зала — векторный чертёж, где каждое место — элемент с уникальным ID. Карта хранится как структурированные данные (ряды, секции, координаты мест) в Postgres, и SVG рендерится клиентски из этих данных.
Сложная часть — конкурентность. Когда 200 человек просматривают одно мероприятие, они должны видеть актуальную доступность мест в реальном времени. Наша модель:
- Состояния мест: доступно, заблокировано, продано. Заблокировано — значит «кто-то добавил в корзину».
- Длительность блокировки: 8 минут. Если не завершишь чекаут за 8 минут — место возвращается в доступные. Это Redis TTL — блокировка буквально истекает.
- Трансляция: При изменении состояния места WebSocket-сообщение уходит всем подключённым клиентам этого мероприятия. Карта перерисовывает только затронутые места — без полной перезагрузки.
- Обработка гонок: Два пользователя кликают на одно место одновременно. Блокировка — Redis
SET NX(установить, если не существует) — атомарная операция, первый записавший побеждает. Проигравший получает тост «место только что забрали» в течение 100ms.
Ноль инцидентов перепродажи за шесть месяцев продакшена. Это метрика, которая важнее всего.
Поток чекаута
Карта зала → сводка корзины → Stripe Checkout → страница подтверждения с QR-кодами.
Весь поток укладывается в 30 секунд для возвращающегося пользователя. Ключевые решения:
- Stripe Checkout, не Elements. Мы используем хостинговую страницу чекаута Stripe, не кастомную форму. Быстрее собрать, PCI-совместима по умолчанию, поддерживает Apple Pay / Google Pay без дополнительной работы. Редирект добавляет один шаг, но убирает месяц работы по PCI-соответствию.
- Генерация QR при покупке. Каждый билет получает уникальный QR-код, сгенерированный серверно сразу после подтверждения оплаты Stripe. QR кодирует подписанный JWT с ID билета, ID мероприятия и местом. Не нужен запрос к базе при сканировании — сканер просто верифицирует подпись.
- PDF-билеты через R2. Каждый билет рендерится как PDF (название мероприятия, место, QR-код, фрагмент карты зала) и хранится в Cloudflare R2. Письмо подтверждения ссылается на PDF. Покупатель также может открыть билеты из аккаунта.
QR-чекин
Сканер для персонала — отдельная страница в том же приложении, за авторизацией. Сотрудник открывает на телефоне, наводит камеру на QR-код и получает:
- Зелёная галочка: валидный билет, ещё не отсканирован. Отмечает как прошедшего.
- Жёлтое предупреждение: уже отсканирован (показывает когда и каким сканером).
- Красный X: невалидный или просроченный билет.
Время ответа — менее 200ms. Сканер работает и офлайн — кеширует список билетов мероприятия локально и синхронизируется при появлении связи. Подробнее об офлайн-архитектуре.
Пропускная способность персонала выросла с ~120 посетителей/час (ручная проверка по бумажным спискам) до ~500/час на сканер. На 3,000-местном мероприятии с 4 сканерами — полный чекин за 25 минут вместо 2+ часов.
Цифры
Шесть месяцев продакшн-данных:
| Метрика | Значение |
|---|---|
| Продано билетов | 12,400+ |
| Аптайм | 99.95% |
| Завершение чекаута (мобилка) | 78% |
| Время чекаута (медиана) | 24s |
| Латенси API p95 | 180ms |
| Инцидентов перепродажи | 0 |
| Экономия на реселлере | ~€18K/год |
78% завершений мобильного чекаута — заголовочная цифра — вместо 60% на старой платформе реселлера. Ещё не 100%, но оставшиеся 22% — в основном чувствительность к цене и брошенный просмотр, а не UX-трение.
€18K/год экономии на комиссиях реселлера окупили всю разработку за первый год.
Что удивило
Производительность SVG-карты мест на дешёвых Android. SVG на 3,000 мест — это 3,000+ DOM-узлов. На Pixel 4a первый рендер был 1.8 секунды, а зум дёргался. Решили виртуализацией — только места в зоне видимости рендерятся как интерактивные элементы. Места за пределами — один элемент <path>, показывающий общую компоновку. Время взаимодействия упало до 200ms.
Порядок Stripe-вебхуков. Вебхуки не приходят по порядку. checkout.session.completed приходил раньше payment_intent.succeeded примерно в 12% случаев. Наша логика фулфилмента изначально зависела от payment_intent.succeeded — а значит, 12% покупателей ждали до 30 секунд свои билеты. Исправили: триггер фулфилмента на checkout.session.completed, а payment_intent.succeeded — как подтверждение, не триггер.
Длительность блокировки в 8 минут — это третья попытка. Начали с 15 минут (слишком долго — на популярных мероприятиях большинство мест «заблокированы» браузерами, которые ушли), попробовали 3 минуты (слишком мало — медленно набирающие на мобилке не успевали завершить чекаут), остановились на 8 после A/B-тестирования на реальном трафике.
Что бы сделал иначе
Использовал бы Stripe Embedded Checkout вместо хостингового. На момент разработки встраиваемый вариант Stripe был новым и слабо документированным. Шесть месяцев спустя он стабилен и убирает редирект. Более плавный UX, тот же уровень PCI-соответствия. Переключил бы при следующем крупном обновлении.
Собрал бы админку как отдельное приложение. Сейчас это то же Next.js-приложение с маршрутизацией по ролям через middleware. Работает, но бандл админки включает компоненты карты мест, которые не нужны публичным пользователям, и наоборот. Два приложения, разделяющие Prisma-схему, дали бы чище сборки и лучшее разделение кода.
Добавил бы нагрузочное тестирование раньше. Мы нагрузочно тестировали на 11-й неделе и обнаружили, что WebSocket карты мест выдерживает ~400 одновременных подключений, прежде чем Redis pub/sub начинает тормозить. Достаточно для текущих размеров мероприятий, но потребовался день оптимизации, который прошёл бы плавнее на более ранней стадии.
Бизнес-результат
E7 теперь владеет своим тикетинг-стеком. Данные клиентов идут в CRM. Маркетинговые рассылки уходят предыдущим покупателям (30% открытий на анонсах мероприятий). Мобильный чекаут работает. Комиссия реселлера — в прошлом.
Платформа обработала 50+ мероприятий без единого инцидента перепродажи или пропущенного вебхука. Чекин персонала превратился из бумажного бутылочного горлышка в 25-минутную операцию.
И критично: платформа — актив, а не расход. E7 сейчас рассматривает white-label для других промоутеров в регионе — линия дохода, которой раньше не существовало.
Если вы платите реселлеру 10–15% и у вас достаточно мероприятий, чтобы математика оправдывала — собрать собственную тикетинг-платформу реально за 12 недель, а не за 12 месяцев. Давайте обсудим ваши цифры.