PDF voucher pipeline через Hatchet + Gotenberg на Supabase Postgres

Веб Штурм
  • Hatchet
  • Gotenberg
  • PDF
  • Supabase
  • background-jobs

Когда нужно генерировать платёжные ваучеры, билеты, путёвки и квитанции — типичный соблазн написать «свою платформу документооборота». Не пишите. На спринте Plan 1 PDF Foundation мы собрали production-ready pipeline за две недели из готовых OSS-кирпичей: Hatchet для workflow-оркестрации + Gotenberg для рендеринга + Supabase Postgres как единое хранилище состояния. На сегодня в проде 9 LIVE PDF-шаблонов — 1 voucher для Круизного флота, 1 voucher для Прогулочный флот и 7 версий pf_excursion (с insert-only versioning).

Контекст

Plan 1 PDF Foundation merged 2026-04-26, тег pf-pdf-foundation-v1. До него у нас был привычный путь — inline PHPSpreadsheet в legacy-системе плюс PDFKit на Node для нового стека. Минусы очевидны: нет async-очереди (HTTP-запрос ждёт, пока сгенерируется PDF), нет общего шаблона для двух стеков, нет атомарного отката при поломке.

После — единый voucher-generate workflow, который работает идентично из любой точки системы.

Почему не стали писать «свою очередь»

В 2026 году писать собственный job-runner на Postgres-таблице с polling’ом — это технический долг с момента первой строки. Hatchet (или Temporal, или Inngest) делают всё то же самое, плюс:

  • gRPC-протокол + structured task definition вместо магических JSON
  • OpenTelemetry из коробки — visibility без отдельной интеграции
  • Retry-policies, timeouts, cron-schedules, parent-child workflows на уровне фреймворка
  • DAG-визуализация в дашборде

Hatchet выбрали по критериям task-first архитектуры, gRPC и self-host без lock-in. Запустили в собственном Docker-контейнере на edином Supabase Postgres через изолированную schema hatchet — никакой второй базы, никакой второй платёжки.

Архитектура pipeline

[Customer pays]
 → [Edge Function] insert into voucher_jobs(public_id, kind, payload)
 → [Hatchet trigger] voucher-generate workflow
 → step 1: render-html (Handlebars + voucher_templates)
 → step 2: generate-pdf (POST /forms/chromium/convert/html → Gotenberg)
 → step 3: upload-storage (Supabase Storage)
 → step 4: update-version (insert into voucher_versions)
 → [Customer email] link to https://pf.sputnik-germes.ru/pdf/v/{shortToken}

Реальные тайминги E2E (от postgres trigger до доступного URL): ~3 секунды на холодный старт, ~700 мс на горячий. PDF в Storage, метаданные — в voucher_versions с insert-only valid_from для аудита.

Почему Gotenberg, а не Puppeteer/Playwright

Gotenberg — это Chromium + LibreOffice, упакованные в Go-Docker HTTP API. MIT, 70+ миллионов pull в Docker Hub. По свежим бенчмаркам warm-performance Chromium-генерации — 3 миллисекунды на простой документ, что быстрее, чем большинство DB-запросов [pdf4.dev/blog/html-to-pdf-benchmark-2026].

Что Gotenberg умеет сверх обычного Chromium-tool:

  • PDF encryption через QPDF (AES-256)
  • PDF/A compliance (1a, 2b, 3b) — обязательно для государственных систем
  • Merge / split / metadata editing
  • 6 параллельных Chromium-конверсий на инстанс (configurable)

Puppeteer и Playwright полностью валидные альтернативы, но требуют ручной обвязки HTTP-сервера и ручной реализации PDF/A. Gotenberg даёт это бесплатно.

# Простой вызов из любого языка
curl -X POST http://gotenberg:3000/forms/chromium/convert/html \
 -F files=@voucher.html \
 -F files=@logo.png \
 -F pdfa=PDF/A-2b \
 -o voucher.pdf

Schema isolation вместо отдельной базы

Hatchet требует свою базу для очередей и состояния. Очевидное решение — отдельный Postgres. Но у нас уже Supabase production, и поднимать вторую инстанцию ради Hatchet — дорого по операциям.

Решение: schema isolation внутри одной базы. Создали hatchet-schema, выделили роль hatchet_user с правами только в неё, прокинули через DATABASE_URL=postgres://hatchet_user:.../postgres?search_path=hatchet.

Гочи: пароли в DATABASE_URL должны быть alphanumeric — символы / и + ломают URL parser в Hatchet. Перегенерировали один раз, забыли проблему.

Feature-flag rollback при миграции

Plan 1 был миграцией с PDFKit (старый pipeline) на Hatchet+Gotenberg (новый). Включили feature-flag pdf_engine в voucher_templates:

-- Старый шаблон до миграции
INSERT INTO voucher_templates (kind, engine, html_template, valid_from)
VALUES ('voucher_pf', 'pdfkit_legacy', '...', '2024-01-01');

-- Новый шаблон, активируется по дате тура
INSERT INTO voucher_templates (kind, engine, html_template, valid_from)
VALUES ('voucher_pf', 'gotenberg_v1', '...', '2026-04-26');

Резолвер берёт max(valid_from) ≤ tour_date. При первой же проблеме на проде — INSERT нового template-record с engine=‘pdfkit_legacy’ и текущей датой. Откат за один SQL, без передеплоя.

Что мы из этого вынесли

  1. Best-of-breed OSS лучше custom-платформы. Hatchet + Gotenberg + Supabase = три кирпича, и каждый из них уже отлажен на проектах большего масштаба, чем у нас будет когда-либо.
  2. Schema isolation работает. Не каждой системе нужна отдельная база. Если уже есть production-ready Postgres с бэкапами и мониторингом — изолируйте через schema, экономьте ops-трудозатраты.
  3. Insert-only versioning + feature-flag = безопасный rollback. Когда шаблон документа можно «откатить SQL’ом» без передеплоя — миграции делаются спокойно.

Ссылки

Часто задаваемые вопросы

Что такое Hatchet и Gotenberg и зачем именно эта связка?
Hatchet — open-source workflow-оркестратор (аналог Temporal, но проще), self-hosted на Postgres. Gotenberg — stateless HTTP-сервис, конвертирующий HTML+CSS в PDF через headless Chromium. Связка даёт production-ready PDF-pipeline за 2 недели: Hatchet оркестрирует, Gotenberg рендерит, Supabase хранит состояние и результаты.
Сколько стоит сборка такого pipeline?
Готовые OSS-кирпичи — лицензии $0. Инфра: Hatchet + Gotenberg в Docker — 2 контейнера, ~500 МБ RAM. Self-hosted на любом VPS 4 GB RAM. Разработка кастомных шаблонов под бизнес — 1-2 недели на первый шаблон + ~3-5 дней на каждый последующий. В проде 9 LIVE-шаблонов с генерацией ~3 сек end-to-end.
Можно ли использовать просто wkhtmltopdf или Puppeteer вместо?
Можно для прототипа. wkhtmltopdf устарел (CSS 2.1, проблемы с шрифтами), Puppeteer внутри своего сервиса требует ручной оркестрации и масштабирования. Gotenberg + Hatchet даёт это из коробки: queue, retries, observability, isolated rendering processes. Окупается от ~5 шаблонов и >1000 генераций/мес.
Когда такая архитектура избыточна?
Если нужен 1 PDF-шаблон и <100 генераций/день — достаточно Puppeteer в основном сервисе. Если шаблоны статические (без сложного Handlebars/templating) — pdfme или PDFKit с готовым JSON-описанием. Полный pipeline Hatchet+Gotenberg оправдан от 3+ шаблонов и нагрузки 1000+ документов/день.