Template contract
Template contract
Section titled “Template contract”Mailify’s TemplateRegistry is a flat, file-based store. It does not parse React Email at runtime — the .tsx compilation happens ahead of time, and the Rust side just reads prebuilt HTML + sidecar files.
Directory layout
Section titled “Directory layout”At the path pointed to by MAILIFY_TEMPLATES__PATH (default ./templates-parser/out):
<template_id>/ <locale>.html # required — pre-rendered React Email HTML subject.<locale>.txt # optional — minijinja-rendered at send time text.<locale>.txt # optional — plaintext alternativePlus a catalog.json at the root listing every (id, locale) pair for registry boot.
Example:
templates-parser/out/├── catalog.json├── welcome/│ ├── en.html│ ├── fr.html│ ├── subject.en.txt│ ├── subject.fr.txt│ ├── text.en.txt│ └── text.fr.txt└── password-reset/ ├── en.html └── subject.en.txtMinijinja in HTML
Section titled “Minijinja in HTML”React Email HTML-encodes {{ }} and {% %} during export. Mailify’s post-build.ts step entity-decodes those spans so the server-side minijinja can parse them at render time.
This means:
- Variables:
{{ vars.first_name }},{{ theme.brand_name }},{{ theme.colors.primary }}. - Control flow:
{% if vars.admin %}…{% endif %},{% for item in vars.items %}…{% endfor %}. - Built-in filters:
{{ vars.email | escape }}, etc.
If you skip post-build.ts, your templates will ship with literal {{ vars.foo }} and never resolve.
RenderContext
Section titled “RenderContext”Every render receives:
theme— the fullThemeconfig object, with colors, fonts, logo URL, social links, andextrabag.vars— the caller-supplied JSON blob from the send request.locale— the resolved locale (after fallback chain).
Example minijinja snippet inside a React Email component:
<Text style={{ color: "{{ theme.colors.primary }}" }}> Hi {{ vars.first_name | default("there") }},</Text>Strict mode
Section titled “Strict mode”If templates.strict = true, Mailify fails startup whenever any built-in template id is missing for the default locale. This catches “I renamed a template but forgot to rebuild” before it reaches a user.
What counts as “built-in” is defined in mailify-templates’s registry code — custom user templates are always optional.
Build pipeline
Section titled “Build pipeline”templates-parser/scripts/templates.config.ts (source of truth — ids + metadata) ↓ make gentemplates-parser/emails/<id>.tsx (React Email components) ↓ make build-templates → email exporttemplates-parser/out/<id>/<locale>.html (HTML-encoded placeholders) ↓ post-build.tstemplates-parser/out/<id>/<locale>.html (decoded, + subject/text sidecars) ↓ build ./target/release/mailify./target/release/mailify (reads out/ at boot)Adding a new template
Section titled “Adding a new template”-
Add its entry to
templates-parser/scripts/templates.config.ts:{id: "invoice-reminder",subject: { en: "Payment due: {{ vars.invoice_number }}", fr: "..." },locales: ["en", "fr"],} -
Run
make gen— generates a.tsxscaffold intemplates-parser/emails/and sidecar files. -
Fill in the React Email component.
-
make build-templates— regeneratesout/with your new template. -
Restart Mailify (or
docker compose up -d --force-recreate mailify).
Serving precompiled templates from a different location
Section titled “Serving precompiled templates from a different location”For production installs where you don’t want to ship node_modules + bun, pre-build elsewhere and point Mailify at the output directory:
# on your build machine or in CI:cd templates-parser && bun install && bun run build
# ship templates-parser/out to the server, then:export MAILIFY_TEMPLATES__PATH=/opt/mailify/templatesmailifyThe universal install script (installation) extracts this bundle into ~/.local/share/mailify/templates automatically.