Olympus Docs
CookbookUI & content

Version-control your email templates

Track changes to templates like code

Email templates change: brand updates, copy improvements, A/B tests. Version control them like any other code.

Where templates live

Kratos templates are in:

courier/templates/
├── recovery_code/valid/
│   ├── email.body.gotmpl
│   ├── email.body.plaintext.gotmpl
│   └── email.subject.gotmpl
├── verification_code/valid/
├── login_code/valid/
├── registration_code/valid/
└── ...

Mount as a volume:

ciam-kratos:
  volumes:
    - ./courier/templates:/courier/templates:ro

Reference in kratos.yml:

courier:
  templates:
    recovery_code:
      valid:
        email:
          body: /courier/templates/recovery_code/valid/email.body.gotmpl
          subject: /courier/templates/recovery_code/valid/email.subject.gotmpl

Git the templates

git add courier/templates/
git commit -m "Email: refine recovery template copy"
git push

Tracked in Olympus repo. Diffable.

Template structure

// recovery_code/valid/email.body.gotmpl
Hi {{ .Identity.traits.first_name }},

Use this code to reset your password:

  {{ .Code }}

Or click: {{ .RecoveryURL }}

This code expires in 1 hour.

{{ .OrganizationName }}

Available variables:

  • .Identity: the user.
  • .Code: the OTP.
  • .RecoveryURL: full URL with token.
  • .OrganizationName: configured org name.

Custom: add to kratos.yml:

courier:
  template_override_path: /courier/templates
  templates:
    recovery_code:
      valid:
        email:
          variables:
            organization_name: "Your App"
            support_email: "support@your-domain.com"

Localization

Per-locale templates:

courier/templates/recovery_code/valid/
├── email.body.gotmpl                 (default, English)
├── email.body.fr.gotmpl               (French)
├── email.body.es.gotmpl               (Spanish)
├── email.body.de.gotmpl               (German)

Kratos picks based on identity.traits.locale.

Plaintext alternative

Always provide plaintext alongside HTML:

email.body.gotmpl           (HTML)
email.body.plaintext.gotmpl (text/plain)

For email clients that prefer plain, or strip HTML. Inbox providers also prefer multipart emails (HTML + plaintext) for deliverability.

Reviewing changes

PR diff:

- Subject: Reset your password
+ Subject: Your password reset link

Reviewer comments. Approved. Merged. Next email goes out with new copy.

Preview locally

# Dev script: render template with sample data
node scripts/preview-template.js recovery_code/valid/email.body.gotmpl

Render to HTML / text, view in browser, check appearance.

Visual diff for HTML

For HTML templates, diff visually:

# Tool: Litmus, Email on Acid, or self-hosted
diff-html before.html after.html --open

Side-by-side. Easy to spot misalignments.

Testing rendering

import { compile } from "@some-go-template/lib";

test("recovery email renders", () => {
  const template = fs.readFileSync("courier/templates/recovery_code/valid/email.body.gotmpl");
  const rendered = compile(template).render({
    Identity: { traits: { first_name: "Alice" } },
    Code: "1234",
    RecoveryURL: "https://your-app.com/reset?token=X",
  });
  expect(rendered).toContain("Hi Alice");
  expect(rendered).toContain("1234");
});

Unit-test templates. Catches broken Go syntax, missing variables.

Deliverability testing

Before shipping major template change, test with Mail Tester:

node scripts/send-test-email.js mailtester+xyz@mail-tester.com

Returns score 0-10. Identifies SPF/DKIM issues, spam-triggering words, etc.

Aim for 9+. Don't ship 6 or below.

A/B testing

For copy / subject experiments:

courier:
  templates:
    recovery_code:
      valid:
        email:
          subject: 
            variant_a: /courier/templates/.../subject.a.gotmpl
            variant_b: /courier/templates/.../subject.b.gotmpl

Pre-courier hook picks variant per user:

const variant = hashToBucket(user.id, 2);
const template = variant === 0 ? "variant_a" : "variant_b";

Track open rates per variant.

Compliance check

Some emails require disclaimers (CAN-SPAM, GDPR):

{{ if .IsMarketing }}
  Unsubscribe: {{ .UnsubscribeURL }}
  Address: [Your company address]
{{ end }}

Verify present in all marketing-tagged templates.

Auth emails are usually "transactional", don't require unsubscribe.

Rendering performance

Template rendering is fast (~1 ms per email). Not a bottleneck.

If you have many emails: courier queue is the bottleneck (delivering to SMTP). Templates aren't the issue.

On this page