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) делают всё то же самое, плюс:

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:

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’ом» без передеплоя — миграции делаются спокойно.

Ссылки