PDF Builder Templates¶
Date: 2026-05-29
Problem / motivation¶
The visual PDF builder on the frontend needs a place to persist the document
layouts a tenant designs. Today pdf_templates stores which header/footer
metadata to render per document type (an array of DocumentMetadata IDs), but
it cannot hold the full builder layout — the drag-and-drop canvas state, block
configuration, styling, and bindings the builder produces. That layout is a free
form JSON blob, it is versioned (the builder schema evolves), and it is scoped to
a document entity type (QUOTE, ORDER, INVOICE, …).
This feature adds a dedicated PDF Builder Templates module that stores one versioned JSON layout per builder template, tagged by entity type.
In scope¶
- New top-level resource
pdf_builder_templates. - Fields:
name(str),value(JSONB layout payload),version(str),entity_type(reusesPdfSourceType: INVOICE, ORDER, QUOTE, …),is_default(bool — at most one default per entity type). - Full module chain: model → repository → service → GraphQL (input, response, queries, mutations) → schema auto-mount → migration → tests → docs.
Out of scope¶
- Actually rendering a PDF from the stored layout (the builder/renderer consumes
valueseparately — this module only persists it). - RBAC
Path/Resourcemembers. Mirrors the siblingpdf_templatesmodule, which is settings-level configuration and does not own a dedicated RBAC path. - Migrating any data from
pdf_templates; the two coexist.
Data model changes¶
New table pdf_builder_templates:
| column | type | notes |
|---|---|---|
id |
uuid PK | gen_random_uuid() |
name |
varchar(255) not null | human-readable template name |
entity_type |
smallint not null, index | PdfSourceType enum (QUOTE, ORDER, …) |
value |
jsonb not null | builder layout payload |
version |
varchar(50) not null | builder schema/template version string |
is_default |
boolean not null | default template for its entity type |
created_at |
timestamptz not null | now() |
A partial unique index (entity_type WHERE is_default) enforces at most one
default template per entity type.
Alembic revisions:
add_pdf_builder_templates_table(down_revisionadd_sales_safety_tier_1) — creates the table.add_is_default_to_pdf_builder_templates(down_revisionadd_pdf_builder_templates_table) — addsis_defaultand the one-default-per-entity-type partial unique index.
GraphQL surface¶
- Query
pdfBuilderTemplates(includeEntityType: PdfSourceType): [PdfBuilderTemplate!]! - Query
pdfBuilderTemplate(id: UUID!): PdfBuilderTemplate - Query
defaultPdfBuilderTemplate(entityType: PdfSourceType!): PdfBuilderTemplate— the current default for an entity type - Query
pdfBuilderTemplatesByEntityType(entityType: PdfSourceType!): [PdfBuilderTemplate!]! - Mutation
createPdfBuilderTemplate(template: PdfBuilderTemplateInput!): PdfBuilderTemplate! - Mutation
updatePdfBuilderTemplate(templateId: UUID!, template: PdfBuilderTemplateInput!): PdfBuilderTemplate! - Mutation
deletePdfBuilderTemplate(templateId: UUID!): Boolean!
Types: PdfBuilderTemplate (response), PdfBuilderTemplateInput (input).
RBAC¶
None. Mirrors pdf_templates (no dedicated Path/Resource). No backfill
script required.
Background tasks / cron¶
None.
Frontend contract¶
- The builder serializes its canvas to a JSON object and sends it as
value. versionis the builder's template-schema version string (e.g."1"), so the renderer can branch on layout shape over time.entityTypeis one of thePdfSourceTypeenum values.- List/create/update/delete + fetch-by-entity-type are all available, so the builder can show "templates for Quotes", load one to edit, save a new version, and delete.
- Set
isDefault: trueon create/update to mark a template the default for its entity type; the backend automatically demotes any previous default for that type (one default per entity type). Read the current default withdefaultPdfBuilderTemplate(entityType: …)— this is what the renderer should resolve when no specific template is chosen.
What's being implemented¶
- Model: pdf_builder_template.py
- Repository:
app/graphql/pdf_builder_templates/repositories/pdf_builder_template_repository.py - Service:
app/graphql/pdf_builder_templates/services/pdf_builder_template_service.py - Input/Response:
app/graphql/pdf_builder_templates/strawberry/ - Queries/Mutations:
app/graphql/pdf_builder_templates/queries/,.../mutations/ - Model registration in load_models.py
- Migration
alembic/versions/20260529_add_pdf_builder_templates_table.py - Tests under
tests/graphql/pdf_builder_templates/
What shipped¶
Everything in scope above. The module is fully wired and discoverable through the
auto-discovery in query.py / mutation.py and the DI provider discovery.
Future additions¶
- RBAC path — if PDF builder access needs to be gated independently of
general settings, add a
Path/Resourcepair and a backfill. Deferred because the siblingpdf_templatesmodule has no dedicated path and the builder is settings-level today. - Renderer integration — consume
valuein the PDF generation pipeline. Deferred: out of scope; the renderer team owns that contract.