Skip to main content

Thermal Receipt Templates

FlowPOS generates ESC/POS thermal receipts from Handlebars templates stored in the document_template table. System defaults are seeded at migration time; per-business and per-location overrides require no code changes.

Supported document types

Document type keyReceiptdocumentType enum value
sale_receiptRetail salesaleReceiptThermal
order_receiptRestaurant orderorderReceiptThermal
order_bill_receiptRestaurant bill (cuenta de mesa)orderBillReceiptThermal

Resolution order

For every print job the backend resolves the template in this order:

  1. Location override — a row in location_template_config binding a specific template to this location + document type
  2. Business override — a document_template row where business_id = <businessId> and document_type = <type>
  3. System default — a document_template row where business_id IS NULL and is_system = true
  4. File fallback — the .template.txt file on disk (dev environment / migration lag safety net)

Resolution happens at job-fetch time (GET /document-print-jobs/:id?cols=42). The print-bridge receives pre-rendered Line[] objects and feeds them straight into the ESC/POS encoder — no template logic runs on the bridge side.

Template DSL

Template bodies are Handlebars with a bracket-tag layer on top.

Bracket tags

TagEffect
[BOLD][/BOLD]Bold text
[CENTER][/CENTER]Center-aligned line
[SEP=<char>]Full-width separator, e.g. [SEP==] or [SEP=-]

Unknown tags are stripped silently (safe degradation for future additions).

Handlebars helpers

HelperSignatureDescription
money(amount, currencyCode)Formats a number as a currency string, e.g. Q1.25
truncate(str, n)Truncates a string to n characters
rightAlign(left, right)Pads left and right so right is flush against the right margin
fmtDate(isoDate)Formats an ISO date using the printer's timezone and locale
itemCount(items)Returns the sum of item.quantity across all items
sumItems(items)Returns the sum of item.total across all items
ifDifferent(a, b) blockRenders block only when `
saleItemLine(qty, name, total, currencyCode)Right-aligned Nx Name Q1.25 line
saleDiscountLine(discount, currencyCode)Indented discount row
orderReceiptItemLine(qty, name, total, currencyCode)Right-aligned order item line
orderBillItemRow(qty, name, unitPrice, currencyCode)N P.U. Name Q1.25
orderBillTotalRow(total, currencyCode)Right-aligned item subtotal row
paymentsChangeLine(payments, total, currencyCode)Change row, or empty string if no change due

Column width (_cols) and locale/timezone (_timezone, _locale) are injected automatically into the template context — helpers read them from options.data.root.

System default templates

The three default template files live in:

apps/backend/src/pdf/infrastructure/templates/
sale-receipt-thermal.template.txt
order-receipt-thermal.template.txt
order-bill-receipt-thermal.template.txt

They are seeded into document_template by migration 2026-04-17t02-00-00-seed-thermal-receipt-templates.mjs using stable UUIDs:

UUIDDocument type
f1e2d3c4-b5a6-7890-1234-567890abcdefsaleReceiptThermal
e2d3c4b5-a6f7-8901-2345-67890abcdef1orderReceiptThermal
d3c4b5a6-f7e8-9012-3456-7890abcdef12orderBillReceiptThermal

Changing the system default

Template body only (labels, layout, sections)

  1. Edit the corresponding .template.txt file.
  2. Sync the DB row with a migration or a direct SQL update:
UPDATE document_template
SET html_template = '<new body>'
WHERE id = 'd3c4b5a6-f7e8-9012-3456-7890abcdef12'; -- order-bill
-- f1e2d3c4-b5a6-7890-1234-567890abcdef → sale
-- e2d3c4b5-a6f7-8901-2345-67890abcdef1 → order receipt

The .template.txt file is the source of truth — always keep it in sync with the DB row.

Adding new formatting capabilities

  1. Register a new Handlebars helper in ensureHelpers()thermal-template-renderer.adapter.ts
  2. If the new capability requires a new bracket tag (e.g. [DW] for double-width), extend parseLine() in the same file and add the new property to the Line type
  3. If the bridge needs to encode the new property, update encodeLines() in renderer.ts
  4. Use the new tag or helper in the .template.txt file

Per-client custom template

No code or deploy needed.

  1. In the PWA admin go to Templates → New template
  2. Set documentType to the relevant thermal type (e.g. orderBillReceiptThermal)
  3. Set businessId to the client's business ID
  4. Write the custom DSL body
  5. Optionally bind it to a specific location via Location Template Config

The resolver will automatically serve the business or location override for that client and fall back to the system default for everyone else.

Payload variables reference

Sale receipt (saleReceiptThermal)

VariableTypeDescription
businessNamestringBusiness display name
businessAddressstring?Business address
businessTaxIdstring?NIT
locationNamestring?Location name
receiptNumberstring?Internal receipt number
documentNumberstring?FEL or alternative document number
saleDatestringISO date of the sale
customerNamestring?Customer name
customerTaxIdstring?Customer NIT
itemsarray{ quantity, name, total, discount? }
subtotalnumberPre-tax subtotal
taxBreakdownarray{ label, amount }
discountTotalnumber?Total discount applied
totalnumberGrand total
currencyCodestringe.g. GTQ
paymentsarray{ method, amount, reference? }
changenumber?Change due
felAuthorizationstring?FEL authorization code
felNumberstring?FEL document number
felSerialNumberstring?FEL series

Order receipt (orderReceiptThermal)

VariableTypeDescription
businessNamestringBusiness display name
locationNamestring?Location name
orderNumberstringOrder number
orderDatestringISO date
tableNumberstring?Table number
serverNamestring?Server/waiter name
itemsarray{ quantity, name, total, modifiers?, notes? }
subtotalnumberPre-tax subtotal
taxBreakdownarray{ label, amount }
totalnumberGrand total
currencyCodestringe.g. GTQ

Order bill receipt (orderBillReceiptThermal)

VariableTypeDescription
businessNamestringBusiness display name
businessAddressstring?Business address
businessTaxIdstring?NIT
locationNamestring?Location name
billNumberstring?Bill number
orderNumberstringSource order number
billDatestringISO date
tableNumberstring?Table number
serverNamestring?Server/waiter name
customerNamestring?Customer name
customerTaxIdstring?Customer NIT
itemsarray{ quantity, name, unitPrice, total, compReason?, modifiers?, notes? }
subtotalnumberPre-tax subtotal
taxBreakdownarray{ label, amount }
tipAmountnumber?Tip amount
totalnumberGrand total
currencyCodestringe.g. GTQ
paymentsarray?{ method, amount, reference? }

Preview paths — DSL renderer vs structured layout

There are two separate preview paths in the system. They are not interchangeable.

PathWhere usedHow it works
DSL renderer (ThermalTemplateRendererAdapter)Template Management UI — POST /pdf/templates/thermal-previewRenders the stored html_template DSL body using Handlebars + bracket-tag parsing. Authoritative for custom thermal template editing and preview.
Structured layout (buildDocumentReceiptLines)WebSocket print job previewBuilds receipt lines from a structured DocumentPrintPayload object using the receipt-layout package. Used by the KDS/print bridge flow.

Both outputs are Array<{ text, bold?, center? }> lines and look similar, but they come from different pipelines and may diverge when templates are customized. The DSL renderer is the source of truth for what a custom template body will actually print. For v1 these two paths are maintained in parallel — convergence is tracked as a follow-up.

template_content_kind column

The document_template table has a template_content_kind column (pdf_html | thermal_escpos_dsl) that classifies the rendering contract for each template. It is:

  • Derived automatically from document_type in TemplateRepository on create/update — callers never set it directly.
  • Enforced by a DB CHECK constraint: thermal receipt document types must always be thermal_escpos_dsl.
  • If you add a new thermal document type in a future migration, extend the CHECK constraint in the same migration.

Two options depending on what you want to change:

Option 1 — Edit the template body only (labels, layout, sections) Edit apps/backend/src/pdf/infrastructure/templates/order-bill-receipt-thermal.template.txt (or the sale/order variants).

Then update the seeded DB row so existing deployments pick it up:

UPDATE document_template
SET html_template = '<new body here>'
WHERE id = 'd3c4b5a6-f7e8-9012-3456-7890abcdef12'; -- order-bill
-- f1e2d3c4-b5a6-7890-1234-567890abcdef → sale
-- e2d3c4b5-a6f7-8901-2345-67890abcdef1 → order

Or add a new migration that does the UPDATE — cleaner for multi-env deployments.

Option 2 — Add new formatting capabilities (new tags, new helpers) Add the helper in ensureHelpers() in apps/backend/src/pdf/infrastructure/adapters/thermal-template-renderer.adapter.ts If it's a new bracket tag (e.g. [DW] for double-width), extend parseLine() in the same file If the bridge needs to handle a new line property (e.g. doubleWidth: true), update encodeLines() in apps/print-bridge/src/renderer.ts Then use the new tag/helper in the .template.txt file The .template.txt file is the single source of truth for the default layout. The DB row is just a copy of it seeded at migration time — always keep them in sync.