Skip to content

Template fields ​

Every template (and layout) is a row in the database. This page explains each field — what it's for and when to set it — so you can model your content with confidence. If you only read one section, make it scope & scopeId.

At a glance ​

FieldTypeRequiredDefaultWhat it's for
namestring✅—Stable identifier you render by, e.g. welcome-email.
scopestring—systemWho owns this version — system (shared default) or a custom owner like tenant/user.
scopeIdstring—nullWhich owner inside that scope, e.g. the tenant id. Empty for system.
localestring—enLanguage variant, e.g. en, fr, es.
engineenum—njkTemplate engine that interpolates variables (njk, hbs, ejs, pug).
languageenum✅—Output processor (html, mjml, md, txt).
subjectstring—nullSubject line (emails). Rendered with the engine too.
contenttext✅—The template body.
templateLayoutNamestring—nullName of a layout to wrap this content in.
displayNamestring✅*—Human-friendly label for admin UIs.
descriptiontext—nullNotes about what the template is for.
typestring—nullFree category, e.g. email/sms/push/pdf. For grouping/filtering.
previewContextjson—nullSample data to preview the template without real data.
isActiveboolean—trueSet false to disable without deleting.
iduuidauto—Primary key.
createdAt / updatedAtdateauto—Timestamps.

* displayName is optional on the entity but required by CreateTemplateDto.

The identity of a template

name + scope + scopeId + locale together are unique. That's what lets you store the same welcome-email as a system default, a tenant override, and a French translation — all at once.


Identity fields ​

These four fields decide which template a render resolves to.

name ​

The stable key you render by. Lowercase letters, numbers, - and _ only (e.g. welcome-email, order_receipt). It stays the same across scopes and locales — that's the whole point.

ts
await templates.render({ name: 'welcome-email', /* ... */ });

scope & scopeId — the important one ​

This is the multi-tenant override mechanism, and it's simpler than it looks:

  • scope = who owns this version of the template.
    • system → the shared default your app ships. Everyone gets it unless they have an override.
    • anything else (tenant, user, organization, …) → a custom override owned by a specific entity. The string is yours to choose.
  • scopeId = which specific owner, inside that scope.
    • For system, scopeId is empty (null).
    • For scope: 'tenant', scopeId is the tenant's id.

Think of it as "scope is the kind of owner, scopeId is the exact owner."

Use cases ​

GoalscopescopeId
One default for everybodysystem—
Acme Corp gets its own welcome emailtenantacme
Per-user email signatureuserthe user's id
Region-wide override for EUregioneu

How it resolves ​

When you render, the library looks for the most specific match and falls back to the system default — so you only store overrides where they differ:

ts
// Acme gets its custom version; everyone else falls back to the system default.
await templates.render({
  name: 'welcome-email',
  scope: 'tenant',
  scopeId: 'acme',
  context: { firstName: 'Ada' },
});

If no tenant/acme template exists, it automatically uses the system one. You never have to copy the default into every tenant. See Scopes & locales for the full resolution order.

WARNING

You don't create scope: 'tenant' rows directly — createTemplate() only makes system templates. Create an override from a system template with overwriteSystemTemplate() (or updateTemplate() with a non-system scope). This keeps the shared default safe.

locale ​

The language variant — en, fr, es, … Create the same name once per locale. A request for a missing locale falls back to en.

ts
await templates.createTemplate({ name: 'welcome-email', locale: 'en', content: 'Hello {{ name }}', /* ... */ });
await templates.createTemplate({ name: 'welcome-email', locale: 'fr', content: 'Bonjour {{ name }}', /* ... */ });

Content fields ​

These decide what gets produced.

engine ​

The template engine that interpolates your variables — njk (Nunjucks, default), hbs, ejs or pug. It must be enabled in config.

language ​

The output processor that runs after interpolation:

  • html — pass-through (already HTML).
  • mjml — compile MJML email markup to bulletproof HTML.
  • md — render Markdown to HTML.
  • txt — plain text (SMS, notifications).

Required when you create a template — pick the processor that matches your content (use html if it's already HTML, or txt for plain text). See Engines & languages.

content ​

The template body itself — the thing that gets rendered. Uses your chosen engine's syntax:

html
<h1>Hello {{ firstName }}</h1>
<p>Your order {{ orderId }} has shipped.</p>

subject ​

An optional subject line (for emails). It's rendered with the same engine, so it can use variables too:

Welcome, {{ firstName }}!

The render result returns it separately: { subject, content }.

templateLayoutName ​

The name of a layout to wrap this content in (e.g. a shared email shell). Leave empty for standalone templates.


Metadata fields ​

These don't affect rendering — they help you organize and manage templates, typically in an admin UI.

displayName ​

A human-friendly label, e.g. "Welcome email", shown in dashboards instead of the machine name.

description ​

Free-text notes about what the template is for and who uses it.

type ​

A free category string for grouping and filtering. The TemplateTypeEnum convenience enum suggests email, sms, push, pdf, but any string works. Used by getTemplates({ type }).

previewContext ​

Sample data (JSON) you can use to preview the template without real data — handy for an admin "preview" button. It's not applied automatically during render(); it's there for your tooling.

ts
previewContext: { firstName: 'Ada', orderId: '1234' }

isActive ​

true by default. Set it to false to disable a template without deleting it — inactive templates are skipped during resolution, so rendering falls back to the system default (or throws not-found).


System fields ​

id (uuid primary key) and createdAt / updatedAt are managed for you.


Layouts use the same fields ​

NestDynamicTemplateLayout has the same fields, with one difference: layouts have no subject. Everything above — scope, scopeId, locale, engine, language, isActive — works identically, so you can have per-tenant and per-locale layouts too.

Released under the MIT License.