Invitation Message Templating
The Accounts component uses a small wrapper around jte to render the subject and body of invitation
emails. It is implemented under cards.arda.accounts.util.template; it is not a general template API exposed to other
components.
Runtime flow
Section titled “Runtime flow”TemplateConfiguration reads template.templateDir; the packaged default is /app/conf/. TemplateService searches
that directory for system.jte and system.kte and requires exactly one of them. Other files in the directory are
ignored. Discovery and front-matter parsing happen when the component starts, and the selected template remains loaded
for that process.
The implementation uses a resolver that serves only the selected system template. It does not provide a catalog, runtime template selection, or other named templates to the engine.
Deploying and overriding the template
Section titled “Deploying and overriding the template”The Helm chart stores the default at src/main/helm/config/_shared/system.jte. templates/configmap.yaml combines
files from the shared directory with files from config/<purpose>/:
{{ - $sharedFiles := .Files.Glob "config/_shared/*" }}{{ - $purposeFiles := .Files.Glob (printf "config/%s/*" .Values.global.purpose) }}{{ - $sharedConfig := fromYaml ($sharedFiles.AsConfig | default "{ }") }}{{- $purposeConfig := fromYaml ($purposeFiles.AsConfig | default "{ }") }}{{ - $files := mergeOverwrite (dict) $sharedConfig $purposeConfig }}{{ - (tpl (toYaml $files) .) | nindent 2 }}A purpose can therefore replace the shared template by providing its own file with the same system.jte key. The
resulting ConfigMap is mounted read-only at /app/conf/, alongside application.conf and the other configuration
files. A template change takes effect when the deployment updates and the pod starts with the new ConfigMap content; the
service does not reload templates while running.
Do not deploy both system.jte and system.kte. Startup requires exactly one system template in the mounted
directory.
Template contract
Section titled “Template contract”A template has three parts:
- Required front matter containing string metadata.
- Parameter declarations.
- A subject, one blank line, and then the body; both subject and body may contain interpolated values.
An invitation template:
---contentType: text/plain---@param String invitation_expiresOn@param String invitation_invitee@param String invitation_urlInvitation to Arda Cloud
Hi,
Follow this link to join Arda Cloud: ${invitation_url}The blank line after the rendered subject is significant. TemplateService splits the output with the contract
<first-line subject>\n\n<body> and returns:
data class Message( val metadata: Map<String, String>, val subject: String, val body: String,)Leading or trailing output is not normalized beyond that split. Design the template so the first rendered character is part of the subject and the body starts immediately after the separator.
Content type
Section titled “Content type”contentType defaults to text/plain. When it is exactly text/html, the engine uses HTML output mode and
InvitationService passes the rendered body as EmailMessage.htmlBody. Every other value uses plain output mode and is
passed as textBody.
HTML mode escapes interpolated values according to JTE’s HTML rules. Literal markup authored in the template remains markup. The rendered result must also satisfy the System Email body’s allow-list described in Content constraints.
Front-matter formats
Section titled “Front-matter formats”The parser accepts the following delimiter forms. Metadata keys and values are ultimately converted to strings.
| Format | Opening delimiter | Closing delimiter |
|---|---|---|
| YAML | --- or ---yaml | --- |
| TOML | +++ or ---toml | +++ or ---, respectively |
| JSON | ;;; or ---json | ;;; or ---, respectively |
| JSON object | { | } followed by an empty line |
The first non-empty line must be a supported opening delimiter. Duplicate YAML keys, malformed metadata, a non-object
document, or an unterminated block is rejected. Metadata other than contentType is preserved in Message.metadata,
but invitation delivery currently does not interpret it.
Invitation parameters
Section titled “Invitation parameters”The API accepts messageParameters: Map<String, String> when an invitation is created and persists the map with the
invitation. These values let a purpose-specific template declare additional string parameters.
Immediately before rendering, InvitationService adds three authoritative values:
| Parameter | Value |
|---|---|
invitation_expiresOn | Expiration instant in ISO-8601 form |
invitation_invitee | Invitee email address |
invitation_url | Generated frontend acceptance URL, including the invitee email query parameter |
Caller-supplied keys beginning with invitation_ are removed when the invitation is created. The service then adds its
own values for the three protected parameters before every render. A caller therefore cannot replace the destination,
expiration, or URL used by the system template.
Custom parameters are immutable after creation: invitation updates retain the persisted map and only apply the requested status transition. An unused supplied parameter is harmless. A declared template parameter with no corresponding map entry causes rendering to fail.
Failure behavior
Section titled “Failure behavior”Failure handling depends on where the failure occurs:
| Failure | Observed behavior |
|---|---|
| Missing template directory, zero or two system templates, or invalid front matter | Component initialization fails. |
JTE/KTE render throws TemplateException | Returned as a failed Result; invitation delivery records PENDING_FAILED. |
Rendered message fails EmailMessage validation | Invitation delivery records PENDING_FAILED. |
| Email sender returns a non-success outcome | Invitation delivery records PENDING_FAILED and stores the outcome text in messageStatus. |
| Rendered output lacks the subject/blank-line/body shape | renders() throws IllegalArgumentException; this is not converted to a failed Result by PR #204. |
messageStatus is cleared before a new transition attempt. It contains useful text for a non-success sender outcome;
template and message-construction failures do not currently populate it. A tenant user retries delivery by transitioning
PENDING_FAILED to PENDING, which refreshes the invitation timestamps and renders the current template again.
Verification
Section titled “Verification”Accounts build process finds every .jte and .kte file under the application and test template roots, generates
dummy values for its declared parameters, renders it in isolation, and writes reviewable output under
build/templateCatalog/.
Observability
Section titled “Observability”Accounts emits three metrics.
Successful engine renders increment cards.arda.accounts.util.template.render. Invitation delivery separately counts
cards.arda.accounts.system.invitation.email.send.success and cards.arda.accounts.system.invitation.email.send.failure.
A render that fails inside JTE does not increment the render counter; an output-shape failure occurs after that counter
has already been incremented.
User-configurability
Section titled “User-configurability”The template can be configured without modification of the service code.
Any GitHub user with access to the accounts repository can modify the default
template with a simple Pull-Request against helm/config/_shared/system.jte.
They can also add purpose-specific version of the template with Pull-Request against helm/config/<purpose>/system.jte.
Should new parameters be needed for the template, a Pull-Request must first be opened against the Front-End to collect
and send the desired parameters in the invitation’s messageParameters, a json map of parameters to value.
Once that Pull-Request has been tested, merged and deployed, a 2nd Pull-Request can update the template in accounts.
Copyright: © Arda Systems 2025-2026, All rights reserved