Skip to Content
Templates

Templates

Email content lives next to the notifier consumer: folders under TEMPLATES_DIR (default /app/templates). Each template needs a template.yaml and a body file. Nested folders are fine. Path segments starting with _ are skipped (use _components for shared React pieces).

Every id must be unique across the tree. With email configured, all templates must load or the process will not start.

The example consumer ships welcome, login OTP, and order-receipt templates in MJML, HTML, and React Email (examples/notifier-service/templates). Copy one closest to your layout and change the id / schema.

template.yaml

id: mjml-user-welcome renderer: mjml # react-email | mjml | html; else email.defaults.renderer account: ses # optional email account id from: ${TEMPLATE_FROM_EMAIL} # optional; also fromName, subject, replyTo schema: type: object additionalProperties: false required: [userName, appName] properties: userName: { type: string } appName: { type: string }

schema is required and must be type: object. Supported pieces include required, additionalProperties, enum, string length/pattern/format (email, uri), number min/max, and array items / minItems.

Omit additionalProperties or set it false for strict payloads (unknown keys → 400). Set true only when you intentionally allow extras.

Body files

RendererSourceProduction
react-emailindex.tsx (default export or Email)index.mjs (compiled)
mjmlindex.mjmlcopied; Handlebars then MJML at send
htmlindex.htmlcopied; Handlebars at send

React Email

import { Html, Head, Body, Container, Text } from '@react-email/components'; export default function WelcomeEmail({ userName, appName, }: { userName: string; appName: string; }) { return ( <Html> <Head /> <Body> <Container> <Text>Hello {userName}!</Text> <Text>Welcome to {appName}.</Text> </Container> </Body> </Html> ); }

The runtime image has react and @react-email/render. It does not include tsx or @react-email/components. Compile at image build time.

MJML / HTML (Handlebars)

<mjml> <mj-body> <mj-section> <mj-column> <mj-text>Hello {{userName}}!</mj-text> <mj-text>{{#if isPremium}}Thanks for upgrading.{{/if}}</mj-text> </mj-column> </mj-section> </mj-body> </mjml>
  • Only escaped {{…}}. {{{…}}}, {{&…}} → 400.
  • Missing variables → 400.
  • Unsafe URL schemes in strings (javascript:, data:, vbscript:) are blanked.

Bake into a consumer image

Volume mounts into Distroless are not supported. Rebuild when templates change.

ARG NOTIFIER_IMAGE=ghcr.io/blockqueue/notifier:latest FROM ${NOTIFIER_IMAGE} AS notifier FROM node:24-alpine AS compile WORKDIR /work COPY --from=notifier /app/dist/compile-templates.cjs ./compile-templates.cjs RUN npm init -y && npm install esbuild @react-email/components react react-dom COPY ./templates /templates-src RUN node ./compile-templates.cjs /templates-src /app/templates FROM ${NOTIFIER_IMAGE} COPY --from=compile /app/templates /app/templates COPY ./config/config.yaml /config/config.yaml

Canonical example: examples/notifier-service/Dockerfile.

Offline compile from the monorepo:

npm run compile-templates --workspace=notifier -- <input-dir> <output-dir>

Local vs production load

ModeHowLoads
Local API (npm run dev)TEMPLATES_DIR / CONFIG_PATHSource .tsx / .mjml / .html
Previewexample npm run devPorts 10001 / 10002; not the send API
Production imagebaked pathsindex.mjs + source MJML/HTML

Add a template

  1. New folder under templates/ with template.yaml and body file.
  2. Preview (examples/notifier-service scripts) or hit local API.
  3. Rebuild the consumer image and redeploy notifier.
  4. Call /email/send with the new templateId from any app that has credentials.
Last updated on