PDF voucher pipeline через Hatchet + Gotenberg на Supabase Postgres
- Hatchet
- Gotenberg
- 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, без передеплоя.
Что мы из этого вынесли
- Best-of-breed OSS лучше custom-платформы. Hatchet + Gotenberg + Supabase = три кирпича, и каждый из них уже отлажен на проектах большего масштаба, чем у нас будет когда-либо.
- Schema isolation работает. Не каждой системе нужна отдельная база. Если уже есть production-ready Postgres с бэкапами и мониторингом — изолируйте через schema, экономьте ops-трудозатраты.
- Insert-only versioning + feature-flag = безопасный rollback. Когда шаблон документа можно «откатить SQL’ом» без передеплоя — миграции делаются спокойно.
Ссылки
- Hatchet vs Temporal — почему task-first архитектура
- HTML to PDF benchmark 2026 — warm 3 ms, cold 5-15 с
- Gotenberg vs Chromium-tools 2026 — что умеет уникально
- How to Generate PDFs in 2025 — обзор всех актуальных подходов
- Supabase docs — schema separation — официальный паттерн multi-schema