YYayaw

Transactional Email Templates

Managing React Email transactional templates from the content dashboard.

Yayaw manages transactional email templates from /dashboard/content/email-templates. That route is a Yayaw Table catalog for searching, filtering, creating, deleting, and opening templates. Each row opens the dedicated React Email editor at /dashboard/content/email-templates/[slug].

Related docs:

CMS Role

Transactional email templates are content-managed communication surfaces. They share authoring, localization, variables, and control-plane patterns with the CMS, but they do not render through the public page runtime. Runtime senders load active templates, apply transactional variables, resolve CMS variables, and fall back to React Email component defaults when no database template exists.

Use email templates for purchase confirmations, subscription updates, invitations, and other event-driven messages. Use pages and sections for public or organization-member web content.

Data Model

Each template stores:

  • slug, name, subject, preview_text, description, category
  • rendered html and plain text exports used by runtime sending
  • editor_document, the React Email Editor TipTap JSON document
  • default_locale, locales, and localized_content for inline translated variants
  • variables, the template-specific {{variable}} contract
  • sample_variables, used by the external preview and test sends
  • legacy design, kept only so older Unlayer rows can migrate safely

Templates are platform-wide in v1. Organization-specific overrides should be added later as an explicit lookup layer rather than overloading the current global table.

The production control plane exposes template operations for trusted MCP clients:

  • yayaw_email_templates_list
  • yayaw_email_template_get
  • yayaw_email_template_save
  • yayaw_email_templates_sync_system

Writes require control-plane:write or control-plane:admin, Yayaw email-template:update/manage authorization, and a reason.

Localization

Email templates follow the same inline localization model as CMS pages. The editor toolbar lets authors switch between enabled locales, and each locale stores its own subject, preview text, description, HTML, text export, and editor document inside localized_content.

Saving a locale updates that locale only. The default locale also mirrors the legacy top-level columns so existing runtime lookups and older rows continue to work during migration.

The AI panel includes an enabled-by-default locale sync switch. When a template is saved, the currently open locale is used as the source and the same copy, structure, styling, and block-order changes are applied to the other enabled locales. AI-generated sync updates write localized subject, previewText, description, html, and text values; target-locale editorDocument values are cleared so the editor can rebuild from the synchronized HTML when that locale is opened.

The editor canvas focuses on design only. The toolbar opens the rendered email preview in a separate browser tab after exporting the latest editor state and applying sample transactional variables plus global CMS variables.

Variables

Email templates support two variable families:

  • Template variables use {{name}} or nested paths such as {{invitation.inviteLink}}.
  • CMS variables use the global CMS token format, for example {site.name} or {data.global.header-menu.main.brand_label}.

The email editor exposes both families. CMS variables are resolved through the same published global-data and site-variable catalog used by pages and sections, so content authors can reuse the same global values across the CMS.

Runtime Behavior

When sending an email, Yayaw first tries the active database template for the configured slug and requested locale. If that locale is missing, rendering falls back to the template default locale and then to the legacy top-level columns. If no active template exists, auth emails keep using their React Email component fallback.

Billing emails are also system templates. They are sent from Stripe webhook handlers and are intentionally non-blocking: if Resend is not configured or the template is missing, the webhook logs the skip and continues processing the purchase or subscription update.

Built-in billing template slugs:

  • billing-purchase-confirmation
  • billing-subscription-updated
  • billing-invoice-available

Billing templates can use nested variables such as:

  • {{billing.productName}}
  • {{billing.amountFormatted}}
  • {{billing.billingInterval}}
  • {{billing.billingReason}}
  • {{billing.optionsSummary}}
  • {{billing.lineItemsSummary}}
  • {{billing.discountSummary}}
  • {{billing.periodStart}} and {{billing.periodEnd}}
  • {{billing.invoiceId}}, {{billing.invoiceNumber}}, {{billing.invoiceUrl}}, and {{billing.invoicePdfUrl}}
  • {{billing.manageUrl}}
  • {{billing.accessUrl}}
  • {{billing.deliverablesSummary}}
  • {{organization.name}}
  • {{user.email}}

billing-invoice-available is sent from Stripe invoice.paid events. The handler is idempotent through billing_webhook_events and resolves the organization from invoice metadata, subscription metadata, then local subscription/customer rows. One-time Checkout enables Stripe invoice creation so paid lifetime purchases can also generate this template.

Use bun run docs:llm:generate or the dashboard Sync system templates action after adding billing template metadata so the editor variables and MCP control-plane sync stay aligned.

Stored HTML is treated as the fast runtime representation. Save the template after editing so html, text, and editor_document stay synchronized.

Local Workflow

After changing template contracts, system template metadata, schema, or generated assistant docs, run:

bun run generate:actions
bun run docs:generate
bun run docs:check-translations
bun run docs:llm:generate
bun run docs:llm:check
bunx tsc --noEmit
bun run check

For billing email variables, also run:

bun test src/lib/server/services/billing/billing-email-notifications.test.ts