Skip to main content
Назад к блогу
7 мин чтения

Архитектура билетной платформы на 12K билетов за событие: внутри E7

Полный стек ивент-тикетинга — интерактивные карты залов, Stripe-чекаут, WebSocket в реальном времени, QR-чекин. 12 недель от нуля до продакшена.

case-studynext.jsstripe

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 человек просматривают одно мероприятие, они должны видеть актуальную доступность мест в реальном времени. Наша модель:

  1. Состояния мест: доступно, заблокировано, продано. Заблокировано — значит «кто-то добавил в корзину».
  2. Длительность блокировки: 8 минут. Если не завершишь чекаут за 8 минут — место возвращается в доступные. Это Redis TTL — блокировка буквально истекает.
  3. Трансляция: При изменении состояния места WebSocket-сообщение уходит всем подключённым клиентам этого мероприятия. Карта перерисовывает только затронутые места — без полной перезагрузки.
  4. Обработка гонок: Два пользователя кликают на одно место одновременно. Блокировка — Redis SET NX (установить, если не существует) — атомарная операция, первый записавший побеждает. Проигравший получает тост «место только что забрали» в течение 100ms.

Ноль инцидентов перепродажи за шесть месяцев продакшена. Это метрика, которая важнее всего.

Поток чекаута

Карта зала → сводка корзины → Stripe Checkout → страница подтверждения с QR-кодами.

Весь поток укладывается в 30 секунд для возвращающегося пользователя. Ключевые решения:

QR-чекин

Сканер для персонала — отдельная страница в том же приложении, за авторизацией. Сотрудник открывает на телефоне, наводит камеру на QR-код и получает:

Время ответа — менее 200ms. Сканер работает и офлайн — кеширует список билетов мероприятия локально и синхронизируется при появлении связи. Подробнее об офлайн-архитектуре.

Пропускная способность персонала выросла с ~120 посетителей/час (ручная проверка по бумажным спискам) до ~500/час на сканер. На 3,000-местном мероприятии с 4 сканерами — полный чекин за 25 минут вместо 2+ часов.

Цифры

Шесть месяцев продакшн-данных:

МетрикаЗначение
Продано билетов12,400+
Аптайм99.95%
Завершение чекаута (мобилка)78%
Время чекаута (медиана)24s
Латенси API p95180ms
Инцидентов перепродажи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 месяцев. Давайте обсудим ваши цифры.