Context
The failure this feature prevents is the one every customer has seen:Hola {{first_name}}, landing
in an inbox. It happens when templates accept arbitrary variables and nothing checks them against
real data.
Templates here are React Email components in the repository, versioned like code and reviewed
like code. That is a deliberate trade against a drag-and-drop builder: it means a template change is
a deploy, and it also means a template cannot be broken by a marketer at 6pm on a Friday. Variables
are validated against core.attribute_definitions, so a template referencing a field the tenant
does not have fails at publish rather than at send.
Scope (normative)
messaging.message_templates: versioned, per tenant, per channel.- React Email components with a declared variable contract.
- Variable validation against
core.attribute_definitionsat publish time. - Per-tenant branding: logo, colours, sender name, footer.
- Locale variants of the same template.
- Preview with sample data, and a test send to a chosen address.
Non-scope (normative)
- A drag-and-drop editor. Templates are code (see Context). A builder is a possible later product surface, not a prerequisite.
- Copy translation. The tenant supplies the text per locale; we do not translate for them.
- A/B testing — F2, and it belongs with campaigns rather than with templates.
Behaviour (normative)
- A template is versioned and immutable once published. A send records the template version it used, so a message can be reproduced exactly a year later during a dispute.
- Every template declares its variable contract. Publishing validates each variable against the tenant’s attribute definitions and the event payload shape; an unknown variable blocks publication.
- Rendering with a missing value uses the declared fallback, which is mandatory for every variable. There is no path that renders an empty placeholder or a raw token.
- A template belongs to exactly one channel. The same message on email and push is two templates, because the constraints are not comparable.
- Locale variants share a template id; resolution uses the recipient’s locale, falling back to the tenant’s default. A missing variant falls back rather than failing.
- Marketing templates must include an unsubscribe link, injected by the renderer and not removable by the template author.
- Preview and test sends are rate-limited and audit-logged. A test send goes only to an address the tester controls.
- FORBIDDEN: rendering with an unvalidated variable · a marketing template without an unsubscribe · editing a published version in place.
Data (normative)
API (normative)
Events (normative)
None. Template changes are administrative and covered by the audit log.Acceptance criteria (normative)
- Publishing a template with a variable that does not exist for the tenant is rejected, naming it.
- A published version cannot be edited; editing creates version + 1.
- A send records its template version, and that version renders identically a year later.
- A missing value renders its declared fallback — never an empty placeholder or a raw token.
- A marketing template renders with an unsubscribe link even when the author omitted it.
- A recipient with an unsupported locale receives the tenant’s default variant.
- A test send reaches only the address supplied and is audit-logged.
- Negative: no variable without a declared fallback can be published.
Execution
Single slice, synchronous command. Components live inpackages/email-templates; the registry and
endpoints in backend/api. Rendering happens in the worker, never in the API request.