Edit Page

Email Templates

RESTHeart Cloud

restheart-accounts sends transactional HTML emails for account verification, password reset, and team invitations. All templates are HTML files that support multilingual content via <span lang> elements and dynamic data via {{variable}} placeholders.

Template format

Each template is a self-contained HTML file with two mandatory sections:

  • The <title> element provides the email subject (plain text, after variable substitution).

  • The <body> element provides the email body (HTML, after i18n filtering and variable substitution).

<!DOCTYPE html>
<html>
<head>
  <title>Verify your {{app-name}} account</title>  <!--(1)-->
</head>
<body>
  <p>Hello {{first-name}},</p>  <!--(2)-->
  <p>Click the link below to activate your account:</p>
  <p><a href="{{verification-url}}">Verify email address</a></p>
</body>
</html>
  1. The <title> text becomes the email subject after variable substitution. Keep it short (< 60 characters) and language-neutral, or write it in the application’s primary language.

  2. Variables in {{double-braces}} are replaced at send time.

Internationalization (i18n)

Rules

Wrap locale-specific text in <span lang="xx"> elements, where xx is a BCP 47 language tag (e.g. en, it, fr). When the plugin renders a template it:

  1. Resolves the target locale (see [_locale_resolution] below).

  2. Removes all <span lang> elements whose lang attribute does not match the resolved locale.

  3. Unwraps (keeps inline) all <span lang> elements that do match.

  4. Text nodes and elements without a lang attribute are always rendered regardless of locale.

Locale resolution

The locale is resolved in the following order:

  1. The user document’s locale field, if present.

  2. The default-locale value from accountsConfig.

  3. en as the ultimate fallback.

If the resolved locale produces no visible text (i.e. all text is wrapped in non-matching <span lang> elements), the engine re-runs with en. If there is still no en content, all <span lang> elements are rendered without filtering.

Full example

<!DOCTYPE html>
<html>
<head>
  <title>Verify your {{app-name}} account</title>
</head>
<body>
  <p>
    <span lang="en">Hello {{first-name}},</span>
    <span lang="it">Ciao {{first-name}},</span>
    <span lang="fr">Bonjour {{first-name}},</span>
  </p>
  <p>
    <span lang="en">Please click the link below to verify your email address.</span>
    <span lang="it">Clicca il link qui sotto per verificare il tuo indirizzo email.</span>
    <span lang="fr">Veuillez cliquer sur le lien ci-dessous pour vérifier votre adresse e-mail.</span>
  </p>
  <p>
    <a href="{{verification-url}}">
      <span lang="en">Verify email address</span>
      <span lang="it">Verifica indirizzo email</span>
      <span lang="fr">Vérifier l'adresse e-mail</span>
    </a>
  </p>
  <!-- No lang wrapper — rendered for every locale -->
  <p>{{app-name}} · <a href="{{frontend-url}}">{{frontend-url}}</a></p>
</body>
</html>

For an Italian user (locale: it) this produces:

<p>Ciao Alice,</p>
<p>Clicca il link qui sotto per verificare il tuo indirizzo email.</p>
<p><a href="https://dev.example.com/auth/verify?email=alice@example.com&token=…">Verifica indirizzo email</a></p>
<p>8x5 · <a href="https://dev.example.com">https://dev.example.com</a></p>

Variable substitution

All {{variable}} placeholders are replaced with real values before sending. Unrecognised variables are replaced with an empty string.

Verification template (verification.html)

Variable Description

{{app-name}}

Application name (from accountsConfig.app-name)

{{first-name}}

Recipient’s first name

{{email}}

Recipient’s email address

{{frontend-url}}

Public-facing frontend URL (from accountsConfig.frontend-url)

{{verification-url}}

One-time email verification link (expires after 7 days)

{{year}}

Current year (e.g. 2026)

Password reset template (password-reset.html)

Variable Description

{{app-name}}

Application name

{{first-name}}

Recipient’s first name

{{email}}

Recipient’s email address

{{frontend-url}}

Public-facing frontend URL

{{reset-url}}

One-time password reset link (expires after 1 hour)

{{year}}

Current year (e.g. 2026)

Invite template (invite.html)

Variable Description

{{app-name}}

Application name

{{first-name}}

Invited user’s first name (empty string for new users)

{{email}}

Invited user’s email address

{{frontend-url}}

Public-facing frontend URL

{{invite-url}}

One-time activation link (expires after 7 days)

{{inviter-name}}

Full name of the user who sent the invitation

{{team-name}}

Name of the team the user is being invited to join

{{role}}

Invited role (e.g. user)

{{year}}

Current year (e.g. 2026)

Configuration

Specify the path to each custom template under accountsConfig.templates. Paths are resolved relative to the RESTHeart working directory.

accountsConfig:
  # … other settings …
  templates:
    verification:   etc/email-templates/verification.html
    password-reset: etc/email-templates/password-reset.html
    invite:         etc/email-templates/invite.html

Any key that is absent or set to an empty string causes the built-in template for that email type to be used.

Custom templates

Exporting the built-in templates

The built-in templates are bundled inside the plugin JAR under email-templates/. To use them as a starting point, extract them with:

jar xf plugins/restheart-accounts.jar email-templates/

This creates an email-templates/ directory containing:

  • email-templates/verification.html

  • email-templates/password-reset.html

  • email-templates/invite.html

Move the files to the location configured in accountsConfig.templates, edit as needed, then restart RESTHeart.

Best practices

  • Keep HTML simple — many email clients have limited CSS support. Use inline styles.

  • Always include at least an en block for every localised section so the fallback mechanism has content to display.

  • Subject lines should remain under 60 characters after variable substitution to prevent truncation on mobile clients.

  • Test templates with both HTML-capable clients (Gmail, Outlook) and plain-text fallback views.

Fallback behavior

Condition Result

accountsConfig.templates.<key> not set or empty

Built-in template is used

Configured path does not exist at startup

Built-in template is used; a WARN message is logged

Configured path exists at startup

Custom template is loaded and cached in memory

Note
Template caching occurs at plugin startup. If you modify a template file on disk, restart RESTHeart for the change to take effect.