@antelopejs/interface-dms 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +70 -68
  3. package/dist/auth/db/models/sessions.model.d.ts +1 -0
  4. package/dist/auth/db/models/userExternalIdentities.model.d.ts +1 -0
  5. package/dist/auth/db/models/users.model.d.ts +1 -0
  6. package/dist/base/block-registry/index.d.ts +1 -0
  7. package/dist/base/block-registry/index.js +1 -0
  8. package/dist/base/block-registry/index.js.map +1 -1
  9. package/dist/base/block-registry/presentation.d.ts +21 -0
  10. package/dist/base/block-registry/presentation.js +108 -0
  11. package/dist/base/block-registry/presentation.js.map +1 -0
  12. package/dist/base/block-types.d.ts +1 -0
  13. package/dist/base/block-types.js +1 -0
  14. package/dist/base/block-types.js.map +1 -1
  15. package/dist/base/live-toggle.d.ts +2 -2
  16. package/dist/base/period-selector.d.ts +2 -2
  17. package/dist/base/placeholder.js +2 -2
  18. package/dist/base/placeholder.js.map +1 -1
  19. package/dist/base/table-view/schema.d.ts +4 -4
  20. package/dist/db/internal/exportJobs.model.d.ts +1 -0
  21. package/dist/db/internal/inviteResolutions.model.d.ts +1 -0
  22. package/dist/db/internal/tenantLifecycle.model.d.ts +1 -0
  23. package/dist/db/models/roles.model.d.ts +1 -0
  24. package/dist/db/models/tenantMembers.model.d.ts +1 -0
  25. package/dist/db/models/tenants.model.d.ts +1 -0
  26. package/dist/db/models/user_invites.model.d.ts +1 -0
  27. package/docs/.navigation.yml +2 -0
  28. package/docs/1.core/.navigation.yml +2 -0
  29. package/docs/1.core/1.introduction.md +169 -0
  30. package/docs/1.core/10.layout-banners.md +133 -0
  31. package/docs/1.core/2.pages-and-modules.md +649 -0
  32. package/docs/1.core/3.components.md +287 -0
  33. package/docs/1.core/4.permissions.md +279 -0
  34. package/docs/1.core/5.tenancy.md +358 -0
  35. package/docs/1.core/6.invites.md +239 -0
  36. package/docs/1.core/7.hooks.md +354 -0
  37. package/docs/1.core/8.realtime.md +147 -0
  38. package/docs/1.core/9.quick-actions.md +115 -0
  39. package/docs/2.auth/.navigation.yml +2 -0
  40. package/docs/2.auth/1.introduction.md +75 -0
  41. package/docs/2.auth/2.authentication.md +222 -0
  42. package/docs/2.auth/3.tokens-and-sessions.md +352 -0
  43. package/docs/2.auth/4.users.md +256 -0
  44. package/docs/2.auth/5.external-identities.md +122 -0
  45. package/docs/3.base/.navigation.yml +2 -0
  46. package/docs/3.base/1.introduction.md +202 -0
  47. package/docs/3.base/10.display-blocks.md +136 -0
  48. package/docs/3.base/2.types.md +402 -0
  49. package/docs/3.base/3.forms.md +349 -0
  50. package/docs/3.base/4.tables.md +608 -0
  51. package/docs/3.base/5.charts-and-widgets.md +466 -0
  52. package/docs/3.base/6.layout.md +437 -0
  53. package/docs/3.base/8.data-types.md +361 -0
  54. package/docs/3.base/9.helpers.md +414 -0
  55. package/docs/4.notifications/.navigation.yml +2 -0
  56. package/docs/4.notifications/1.introduction.md +81 -0
  57. package/docs/4.notifications/2.categories-and-subjects.md +214 -0
  58. package/docs/4.notifications/3.sending.md +285 -0
  59. package/docs/5.html-render/.navigation.yml +2 -0
  60. package/docs/5.html-render/1.introduction.md +386 -0
  61. package/docs/5.html-render/2.built-in-emails.md +157 -0
  62. package/docs/images/emails-admin-invite.png +0 -0
  63. package/docs/images/emails-change-notice.png +0 -0
  64. package/docs/images/emails-change-verification.png +0 -0
  65. package/docs/images/emails-digest-logo-dark.png +0 -0
  66. package/docs/images/emails-digest-logo-light.png +0 -0
  67. package/docs/images/emails-digest.png +0 -0
  68. package/docs/images/emails-export-partial.png +0 -0
  69. package/docs/images/emails-export-ready.png +0 -0
  70. package/docs/images/emails-reset-password.png +0 -0
  71. package/docs/images/emails-two-factor.png +0 -0
  72. package/docs/images/emails-user-validation.png +0 -0
  73. package/docs/index.md +109 -0
  74. package/docs/tsconfig.json +11 -0
  75. package/package.json +25 -18
@@ -0,0 +1,386 @@
1
+ ---
2
+ title: "Introduction"
3
+ description: "Turn a named template and a props object into an HTML string, rendered by the frontend and delivered by your code — as an email, a file or a response."
4
+ ---
5
+
6
+ # Introduction
7
+
8
+ ## Overview
9
+
10
+ The `html-render` domain of `@antelopejs/interface-dms` turns a named template plus a props object into an HTML string. You declare a template once with `RegisterHtmlTemplate`, which records the name in a process-wide registry and hands back a typed reference; passing that reference to `GenerateHtml` produces the rendered HTML.
11
+
12
+ Rendering itself does not happen in the backend process. `GenerateHtml` performs an HTTP `POST` to the render endpoint configured on the DMS (`htmlRender.renderEndpoint`), authenticated with a signed service token, and returns the HTML the endpoint sends back. The backend therefore owns the template *names* and the props *types*; the markup lives with the renderer behind that endpoint.
13
+
14
+ The interface produces HTML and nothing else. Delivering that HTML — as a transactional email, as a file, as a response body — is your code's job.
15
+
16
+ Everything below is exported from a single subpath:
17
+
18
+ ```typescript
19
+ import {
20
+ GenerateHtml,
21
+ GetAllHtmlTemplates,
22
+ GetHtmlTemplate,
23
+ RegisterHtmlTemplate,
24
+ } from "@antelopejs/interface-dms/html-render";
25
+ import type {
26
+ HtmlRenderConfig,
27
+ HtmlTemplateInfo,
28
+ HtmlTemplateRef,
29
+ } from "@antelopejs/interface-dms/html-render";
30
+ ```
31
+
32
+ ## Key Features
33
+
34
+ - **Named template registry** — `RegisterHtmlTemplate` records a template name at import time, so the whole process shares one catalogue of renderable templates.
35
+ - **Documented props** — the type parameter on `RegisterHtmlTemplate` travels with the returned `HtmlTemplateRef`, recording on the reference itself which props the template expects.
36
+ - **Remote rendering over HTTP** — `GenerateHtml` calls the configured render endpoint with a signed service token and resolves to the returned HTML string.
37
+ - **Registry inspection** — `GetHtmlTemplate` and `GetAllHtmlTemplates` read back what has been registered, one name at a time or in bulk.
38
+ - **Configurable endpoint and token** — `HtmlRenderConfig` types the `htmlRender` block of the DMS module configuration: endpoint URL, signing secret, token lifetime.
39
+
40
+ ## Dependencies
41
+
42
+ This interface relies on the following AntelopeJS packages:
43
+
44
+ - [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - `InterfaceFunction` and `RegisteringProxy`, the primitives the interface is built from.
45
+
46
+ ## Quick Start
47
+
48
+ Register a template next to the code that uses it, then render it with props.
49
+
50
+ ```typescript
51
+ import {
52
+ GenerateHtml,
53
+ RegisterHtmlTemplate,
54
+ } from "@antelopejs/interface-dms/html-render";
55
+
56
+ interface WelcomeProps {
57
+ userName: string;
58
+ activationLink: string;
59
+ }
60
+
61
+ // Registration happens at import time; keep the ref in module scope.
62
+ const WelcomeTemplate = RegisterHtmlTemplate<WelcomeProps>("EmailWelcome");
63
+
64
+ export async function buildWelcomeHtml(
65
+ userName: string,
66
+ activationLink: string,
67
+ ): Promise<string> {
68
+ return GenerateHtml(WelcomeTemplate, { userName, activationLink });
69
+ }
70
+ ```
71
+
72
+ `GenerateHtml` needs the render endpoint to be reachable, so it belongs in request or job code — do not call it at import time.
73
+
74
+ ## RegisterHtmlTemplate
75
+
76
+ Registers a template name and returns the reference you render with.
77
+
78
+ ```typescript
79
+ function RegisterHtmlTemplate<TProps = unknown>(
80
+ name: string,
81
+ ): HtmlTemplateRef<TProps>;
82
+ ```
83
+
84
+ | Parameter | Description |
85
+ | --------- | ----------- |
86
+ | `name` | The template identifier. It is the key the registry stores the template under, and the `templateName` sent to the render endpoint. |
87
+
88
+ The type parameter `TProps` declares the props the template expects. It is carried by the returned [`HtmlTemplateRef`](#htmltemplateref) as a record of the template's shape; [`GenerateHtml`](#generatehtml) takes its `props` argument as `unknown`, so the compiler does not check the two against each other at the call site. Omitting `TProps` leaves the reference's props type as `unknown`.
89
+
90
+ ```typescript
91
+ import { RegisterHtmlTemplate } from "@antelopejs/interface-dms/html-render";
92
+
93
+ interface InvoiceProps {
94
+ invoiceNumber: string;
95
+ amount: string;
96
+ dueDate: string;
97
+ }
98
+
99
+ export const InvoiceTemplate =
100
+ RegisterHtmlTemplate<InvoiceProps>("EmailInvoice");
101
+ ```
102
+
103
+ The registry is keyed by name, so registering an existing name replaces the earlier entry. The DMS registers seven names for its own emails, listed in [Built-in Emails](/docs/interfaces/html-render/built-in-emails) — pick different names for your templates.
104
+
105
+ Call `RegisterHtmlTemplate` at module scope, in a file your module imports during startup. The returned reference is a plain object, so exporting it from that module is the usual way to share it.
106
+
107
+ The subpath also exports an `internal` namespace holding the registering proxy this function writes through. It is implementation detail — register through `RegisterHtmlTemplate`.
108
+
109
+ ## GenerateHtml
110
+
111
+ Renders a registered template and resolves to the resulting HTML.
112
+
113
+ ```typescript
114
+ const GenerateHtml: (
115
+ template: HtmlTemplateRef<unknown>,
116
+ props: unknown,
117
+ ) => Promise<string>;
118
+ ```
119
+
120
+ | Parameter | Description |
121
+ | --------- | ----------- |
122
+ | `template` | The reference returned by `RegisterHtmlTemplate`. Pass the reference, not the name string. |
123
+ | `props` | The data the template renders with, serialised into the request body. The render endpoint expects a JSON object and rejects anything else with a `400`, so keep the props type an object shape. Typed `unknown` at this boundary, so the props type recorded on the reference is not enforced here — match it yourself. |
124
+
125
+ ```typescript
126
+ import { GenerateHtml } from "@antelopejs/interface-dms/html-render";
127
+ import { InvoiceTemplate } from "./templates";
128
+
129
+ export async function renderInvoice(invoiceNumber: string): Promise<string> {
130
+ return GenerateHtml(InvoiceTemplate, {
131
+ invoiceNumber,
132
+ amount: "240.00 EUR",
133
+ dueDate: "2026-08-15",
134
+ });
135
+ }
136
+ ```
137
+
138
+ The call `POST`s `{ templateName, props }` as JSON to `htmlRender.renderEndpoint`, with a service token signed from `htmlRender.serviceSecret` in the `x-dms-service-token` header, and reads the `html` field out of the JSON response.
139
+
140
+ `GenerateHtml` does not consult the registry before calling out: it sends whatever name the reference carries. A name the render endpoint does not know surfaces as a rejected promise, not as a missing-template check on the backend.
141
+
142
+ On a non-2xx response the promise rejects with an `Error` whose message starts with `Failed to render HTML template:`, followed by the response body's `error` string, its `statusMessage`, or `HTTP <status>`. When the endpoint is unreachable the underlying `fetch` failure propagates instead, without that prefix. Wrap the call when a failed render must not abort the surrounding operation:
143
+
144
+ ```typescript
145
+ import { Logging } from "@antelopejs/interface-core/logging";
146
+ import { GenerateHtml } from "@antelopejs/interface-dms/html-render";
147
+ import { InvoiceTemplate } from "./templates";
148
+
149
+ export async function renderInvoiceOrNull(
150
+ invoiceNumber: string,
151
+ ): Promise<string | null> {
152
+ try {
153
+ return await GenerateHtml(InvoiceTemplate, {
154
+ invoiceNumber,
155
+ amount: "240.00 EUR",
156
+ dueDate: "2026-08-15",
157
+ });
158
+ } catch (error) {
159
+ Logging.Error(`Invoice render failed: ${(error as Error).message}`);
160
+ return null;
161
+ }
162
+ }
163
+ ```
164
+
165
+ ## GetHtmlTemplate
166
+
167
+ Looks a single template up in the registry.
168
+
169
+ ```typescript
170
+ const GetHtmlTemplate: (key: string) => Promise<HtmlTemplateInfo | undefined>;
171
+ ```
172
+
173
+ | Parameter | Description |
174
+ | --------- | ----------- |
175
+ | `key` | The name the template was registered under. |
176
+
177
+ Resolves to the [`HtmlTemplateInfo`](#htmltemplateinfo) stored for that name, or to `undefined` when nothing is registered under it.
178
+
179
+ ```typescript
180
+ import { GetHtmlTemplate } from "@antelopejs/interface-dms/html-render";
181
+
182
+ export async function isTemplateRegistered(name: string): Promise<boolean> {
183
+ return (await GetHtmlTemplate(name)) !== undefined;
184
+ }
185
+ ```
186
+
187
+ ## GetAllHtmlTemplates
188
+
189
+ Resolves to every registered template, keyed by name.
190
+
191
+ ```typescript
192
+ const GetAllHtmlTemplates: () => Promise<Record<string, HtmlTemplateInfo>>;
193
+ ```
194
+
195
+ ```typescript
196
+ import { GetAllHtmlTemplates } from "@antelopejs/interface-dms/html-render";
197
+
198
+ export async function listTemplateNames(): Promise<string[]> {
199
+ return Object.keys(await GetAllHtmlTemplates());
200
+ }
201
+ ```
202
+
203
+ The result is a fresh object built from the registry, so mutating it does not change what is registered. Because registration happens as modules are imported, call this after startup rather than at import time if you want the complete set.
204
+
205
+ ## HtmlTemplateInfo
206
+
207
+ What the registry stores for each template, and what `GetHtmlTemplate` and `GetAllHtmlTemplates` resolve to.
208
+
209
+ | Field | Type | Description |
210
+ | ----- | ---- | ----------- |
211
+ | `name` | `string` | The registered template name. |
212
+
213
+ ```typescript
214
+ import type { HtmlTemplateInfo } from "@antelopejs/interface-dms/html-render";
215
+ import { GetAllHtmlTemplates } from "@antelopejs/interface-dms/html-render";
216
+
217
+ export async function describeTemplates(): Promise<string> {
218
+ const templates: Record<string, HtmlTemplateInfo> =
219
+ await GetAllHtmlTemplates();
220
+
221
+ return Object.values(templates)
222
+ .map((info) => info.name)
223
+ .join(", ");
224
+ }
225
+ ```
226
+
227
+ ## HtmlTemplateRef
228
+
229
+ The handle `RegisterHtmlTemplate` returns and `GenerateHtml` accepts.
230
+
231
+ | Field | Type | Description |
232
+ | ----- | ---- | ----------- |
233
+ | `name` | `string` | The template name sent to the render endpoint. |
234
+ | `_propsType` | `TProps \| undefined` | Type-only carrier for the props type. It is never assigned a value; do not read it. |
235
+
236
+ Annotate the reference explicitly when you collect handles in a shared module and want the props type visible in that module's declarations:
237
+
238
+ ```typescript
239
+ import type { HtmlTemplateRef } from "@antelopejs/interface-dms/html-render";
240
+ import { RegisterHtmlTemplate } from "@antelopejs/interface-dms/html-render";
241
+
242
+ interface AlertProps {
243
+ title: string;
244
+ body: string;
245
+ }
246
+
247
+ export const AlertTemplate: HtmlTemplateRef<AlertProps> =
248
+ RegisterHtmlTemplate<AlertProps>("EmailAlert");
249
+ ```
250
+
251
+ ## HtmlRenderConfig
252
+
253
+ Types the `htmlRender` block of the DMS module configuration in `antelope.config.ts`.
254
+
255
+ | Option | Type | Description |
256
+ | ------ | ---- | ----------- |
257
+ | `renderEndpoint` | `string` | Absolute URL `GenerateHtml` posts to. Default `http://localhost:3001/api/html/render`. |
258
+ | `serviceSecret` | `string` | Secret used to sign the service token that authenticates the render call. Default `"dev"` — set a strong value in production. |
259
+ | `serviceTokenLifetime` | `number` | Lifetime of that service token, in milliseconds. The frontend render server rejects a token valid for more than five minutes. Default `300000` (5 minutes). |
260
+
261
+ The DMS merges the block you supply over those defaults, so a configuration only needs the keys it changes:
262
+
263
+ ```typescript
264
+ import { defineConfig } from "@antelopejs/interface-core/config";
265
+
266
+ const renderSecret = process.env.DMS_HTML_RENDER_SECRET;
267
+ if (!renderSecret) {
268
+ throw new Error("DMS_HTML_RENDER_SECRET is not set");
269
+ }
270
+
271
+ export default defineConfig({
272
+ name: "acme-admin",
273
+ modules: {
274
+ dms: {
275
+ source: {
276
+ type: "package",
277
+ package: "@antelopejs/dms",
278
+ version: ">=0.4.3 <1.0.0",
279
+ },
280
+ config: {
281
+ htmlRender: {
282
+ renderEndpoint: "https://admin.example.com/api/html/render",
283
+ serviceSecret: renderSecret,
284
+ },
285
+ },
286
+ },
287
+ },
288
+ });
289
+ ```
290
+
291
+ The Configuration chapter of the DMS guide covers where this block sits alongside the other DMS options.
292
+
293
+ ## Email Layout
294
+
295
+ The DMS ships the frame of its own emails as `EmailLayout`, which a module's email template (registered through `dms.email.ts`) may reuse: the branding logo, a card, an optional heading and a `footer` slot below a separator.
296
+
297
+ ```vue
298
+ <script setup lang="ts">
299
+ import EmailLayout from "#dms-layout/app/emails/components/EmailLayout.vue";
300
+
301
+ defineProps<{ subject: string; preheader: string; locale: string }>();
302
+ </script>
303
+
304
+ <template>
305
+ <EmailLayout
306
+ :lang="locale"
307
+ :preheader="preheader"
308
+ :divider="false"
309
+ public-logo-only
310
+ >
311
+ <template #head>
312
+ <title>{{ subject }}</title>
313
+ </template>
314
+ <EText>Your order has shipped.</EText>
315
+ <template #footer>
316
+ <EText>Acme Inc.</EText>
317
+ </template>
318
+ </EmailLayout>
319
+ </template>
320
+ ```
321
+
322
+ | Prop | Type | Description |
323
+ | ---- | ---- | ----------- |
324
+ | `title` | `string` (optional) | Drawn as the `h1` heading. Without it the email has no heading. |
325
+ | `lang` | `string` (optional) | The `lang` of the document. Default `"en"`. |
326
+ | `preheader` | `string` (optional) | Inbox preview text, hidden in the body. |
327
+ | `divider` | `boolean` (optional) | Draws the separator above the `footer` slot. Default `true`. |
328
+ | `logoAlt` | `string` (optional) | Alternative text of the logo. Default `"Logo"`; pass `""` for a decorative logo. |
329
+ | `publicLogoOnly` | `boolean` (optional) | Leaves the logo out unless a mail client can load it (see below). Default `false`. |
330
+
331
+ The `head` slot adds elements to the document head (a `<title>`, a viewport `<meta>`).
332
+
333
+ The logo is `branding.logo.email.light` from the app config, or `branding.logo.default.light` without an email logo, made absolute against `public.dms.clientBaseUrl` when it starts with `/`. A recipient's mail client fetches it from the internet, and Gmail and Outlook refuse SVG: with `publicLogoOnly`, the layout leaves the logo out when it is an SVG, is not an `http(s)` URL, or is served from a host a mail client cannot reach (`localhost`, `*.local`, `*.internal`, a loopback, private or link-local address), rather than drawing a broken image. Set a PNG logo and a public `clientBaseUrl` for it to show. A `branding.logo.email.dark` goes through the same rules and, when it differs from the light one, is offered to the mail clients in dark mode: the logo is then drawn as `<picture><source media="(prefers-color-scheme: dark)" srcset="…"><img …></picture>`, and a client that ignores the source draws the light image. The same check is exported as `emailLogoUrl(logoPath, clientBaseUrl)` from `#dms-layout/app/emails/components/index`, for a template that draws its own header.
334
+
335
+ ### A rendered custom digest
336
+
337
+ The playground's `EmailPlaygroundDigest` reuses this layout with a preheader, `divider="false"` and `public-logo-only`. Here is its actual rendered HTML in English, with fictitious activity for Alex Morgan. With the default SVG logo, the header image is intentionally absent.
338
+
339
+ ![Rendered custom playground digest without a logo or footer divider](../images/emails-digest.png)
340
+
341
+ The same template below uses two demonstration PNG logos supplied through the app config. The light-mode capture selects the teal logo; the dark-mode capture selects the navy logo. The reserved demo hostname is mapped to a local image server for these captures, so they do not establish that a recipient can fetch the logo from the internet.
342
+
343
+ ![Custom digest with the light demonstration logo in browser light mode](../images/emails-digest-logo-light.png)
344
+
345
+ ![Custom digest with the dark demonstration logo in browser dark mode](../images/emails-digest-logo-dark.png)
346
+
347
+ Dark mode changes the selected logo, not the card: `EmailLayout` keeps its white card and pale background in both browser modes. Without a dark logo source, the unbranded examples look identical in both modes. These captures use the real frontend render endpoint and show the returned HTML directly, not the playground's code viewer. No email is sent. Mail clients may strip `<picture>`, block remote images or transform colors; test delivery and appearance in your target clients separately.
348
+
349
+ ## Sending Rendered HTML as Email
350
+
351
+ Rendering and sending are separate steps. Render with `GenerateHtml`, then hand the string to the standard AntelopeJS email interface, which needs an implementing module (such as `@antelopejs/nodemailer`) loaded in your project configuration.
352
+
353
+ ```typescript
354
+ import { Send } from "@antelopejs/interface-email";
355
+ import {
356
+ GenerateHtml,
357
+ RegisterHtmlTemplate,
358
+ } from "@antelopejs/interface-dms/html-render";
359
+
360
+ interface ReceiptProps {
361
+ userName: string;
362
+ orderId: string;
363
+ }
364
+
365
+ const ReceiptTemplate = RegisterHtmlTemplate<ReceiptProps>("EmailReceipt");
366
+
367
+ export async function sendReceipt(
368
+ email: string,
369
+ userName: string,
370
+ orderId: string,
371
+ ): Promise<void> {
372
+ const html = await GenerateHtml(ReceiptTemplate, { userName, orderId });
373
+
374
+ const result = await Send({
375
+ to: email,
376
+ subject: `Your receipt for order ${orderId}`,
377
+ html,
378
+ });
379
+
380
+ if (!result.success) {
381
+ throw new Error(`Failed to send receipt: ${result.error?.message}`);
382
+ }
383
+ }
384
+ ```
385
+
386
+ This is the shape the DMS uses for its own account emails, and it keeps the two failure modes apart: a rejected `GenerateHtml` means the HTML was never produced, while a `success: false` response from `Send` means it was produced but not delivered.
@@ -0,0 +1,157 @@
1
+ ---
2
+ title: "Built-in Emails"
3
+ description: "The DMS sends seven emails of its own."
4
+ ---
5
+
6
+ # Built-in Emails
7
+
8
+ ## Overview
9
+
10
+ The DMS sends seven emails of its own. Each one is a template registered through [`RegisterHtmlTemplate`](/docs/interfaces/html-render/introduction#registerhtmltemplate), rendered by the frontend server from a Vue component in `frontend-vue/layers/dms-layout/app/emails/`, and delivered through `@antelopejs/interface-email`. They share one frame, `EmailLayout`: the branding logo, a white card, a heading, the body, a separator and a footer.
11
+
12
+ | Template | Sent when | Sent to | English subject |
13
+ | -------- | --------- | ------- | ------- |
14
+ | `EmailUserValidation` | A user signs up while `auth.mustValidateEmail` is on, or asks for a new code | The new account's address | `Verify Your Email` |
15
+ | `EmailResetPassword` | A user asks for a password reset | The account's address | `Reset Your Password` |
16
+ | `EmailTwoFactor` | A user signs in with email as their second factor | The account's address | `Your verification code` |
17
+ | `EmailAdminInvite` | A member is invited to the platform or to a workspace | The invited address | `You're invited to join <workspace> on <platform>`, shorter when a name is unknown |
18
+ | `EmailChangeVerification` | A user asks to change their sign-in email | The new address | `Confirm your new sign-in email` |
19
+ | `EmailChangeNotice` | The same request | The current address | `Your sign-in email is being changed` |
20
+ | `EmailExportReady` | A data export delivered by email has finished | The user who asked for it | `Your data export is ready` |
21
+
22
+ Each email, subject included, is written in the recipient's language, English or French (see [Language](#language)).
23
+
24
+ These names are taken: pick other names for your own emails. To change what a built-in email looks like, see [Replacing a built-in email](#replacing-a-built-in-email).
25
+
26
+ ## Props
27
+
28
+ What the backend passes to each template. A template that replaces a built-in one receives the same props.
29
+
30
+ | Template | Props |
31
+ | -------- | ----- |
32
+ | `EmailUserValidation` | `userName`, `validationCode`, `expiresInHours` (from `auth.emailValidationTokenLifetime`), `expiresIn` |
33
+ | `EmailResetPassword` | `userName`, `resetCode`, `expiresInHours` (from `auth.passwordRecoverTokenLifetime`), `expiresIn` |
34
+ | `EmailTwoFactor` | `userName`, `verificationCode` |
35
+ | `EmailAdminInvite` | `email`, `signupLink`, `expiresInDays`, and when known `workspaceName`, `inviterName`, `platformName` (`meta.title`) |
36
+ | `EmailChangeVerification` | `userName`, `newEmail`, `code`, `expiresInHours` (from `auth.emailValidationTokenLifetime`), `expiresIn` |
37
+ | `EmailChangeNotice` | `userName`, `newEmail` |
38
+ | `EmailExportReady` | `userName`, `downloadLink`, `expiresInHours`, `expiresIn`, `partial`, `failedSources` (the export sources that could not be included) |
39
+
40
+ `userName` is the user's name, or their email address when they have none. `expiresInHours` is the lifetime as a number, for the template to word in the recipient's language; `expiresIn` is the same lifetime already worded in English (`24 hours`), kept for templates written before `expiresInHours`.
41
+
42
+ ## Rendered examples
43
+
44
+ These captures show HTML returned by the DMS frontend's real render endpoint, opened directly in Chromium, rather than the playground's HTML code viewer. All names, codes, addresses and links are fictitious; no email is sent. The default SVG logo is intentionally omitted by `publicLogoOnly`.
45
+
46
+ ### EmailUserValidation
47
+
48
+ The signup email asks Alex to verify their address with a six-digit code, valid for 24 hours.
49
+
50
+ ![Rendered signup verification email with a six-digit code and a 24-hour expiry](../images/emails-user-validation.png)
51
+
52
+ ### EmailResetPassword
53
+
54
+ The password-reset email uses a separate code and the configured recovery lifetime: two hours in this example.
55
+
56
+ ![Rendered password-reset email with a two-hour code expiry](../images/emails-reset-password.png)
57
+
58
+ ### EmailTwoFactor
59
+
60
+ The second-factor email provides a sign-in code. Its footer tells the recipient to ignore the message if they did not attempt to sign in.
61
+
62
+ ![Rendered email second-factor verification code](../images/emails-two-factor.png)
63
+
64
+ ### EmailAdminInvite
65
+
66
+ The invitation names the inviter, workspace and platform when they are known. Its button uses a fictitious destination here.
67
+
68
+ ![Rendered invitation from Jordan Lee to Northwind Studio on Acme Dashboard](../images/emails-admin-invite.png)
69
+
70
+ ### EmailChangeVerification
71
+
72
+ The new address receives the code that confirms the sign-in email change.
73
+
74
+ ![Rendered confirmation email naming the new sign-in address and verification code](../images/emails-change-verification.png)
75
+
76
+ ### EmailChangeNotice
77
+
78
+ The current address receives a notice, not the confirmation code. It tells the recipient what to do if they did not request the change.
79
+
80
+ ![Rendered security notice sent to the current sign-in address](../images/emails-change-notice.png)
81
+
82
+ ### EmailExportReady
83
+
84
+ A complete export provides a download button and an expiry. A partial export also names the sources that could not be included.
85
+
86
+ ![Rendered complete data-export email with a download button and expiry](../images/emails-export-ready.png)
87
+
88
+ ![Rendered partial data-export email warning that Activity history is missing](../images/emails-export-partial.png)
89
+
90
+ These are browser previews, not Gmail, Outlook or Apple Mail compatibility tests. See [Email Layout](/docs/interfaces/html-render/introduction#email-layout) for the custom digest, logo variants and the limits of dark-mode support.
91
+
92
+ ## Language
93
+
94
+ Each email is written in the recipient's language: the account's `language`, or for an invitation the language it was sent in. English and French are supported; any other language falls back to English.
95
+
96
+ The body text lives in the `dms-layout` locale files (`i18n/locales/layout-*.json`), under one key per email (`emails.user_validation`, `emails.reset_password`, `emails.two_factor`, `emails.admin_invite`, `emails.email_change_verification`, `emails.email_change_notice`, `emails.export_ready`) plus the shared `emails.common`. Locale files merge in module priority order, so a module registered with a priority above the DMS's (`0`) can reword any of these keys in its own `i18n/locales/*.json`:
97
+
98
+ ```json [frontend-vue/i18n/locales/acme-en-GB.json]
99
+ {
100
+ "emails": {
101
+ "reset_password": {
102
+ "footer": "Didn't ask for this? Contact support@acme.test."
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ The subject is set by the backend, which sends it in the same language. It cannot be reworded from a module.
109
+
110
+ ## Logo
111
+
112
+ Every built-in email draws `branding.logo.email.light` from the app config, or `branding.logo.default.light` when no email logo is set (see [Email Layout](/docs/interfaces/html-render/introduction#email-layout)), with `publicLogoOnly` on. The logo shows only when a mail client can load it: a PNG or JPEG served from a public `http(s)` address. The default DMS logo is an SVG, which Gmail and Outlook refuse, so out of the box the emails carry no logo. To show yours, set an `email` logo in your layer's `app/app.config.ts`, and serve the dashboard from a public `clientBaseUrl`:
113
+
114
+ ```ts [frontend-vue/app/app.config.ts]
115
+ export default defineDmsAppConfig({
116
+ branding: {
117
+ logo: {
118
+ email: {
119
+ light: "/branding/logo-email.png",
120
+ dark: "/branding/logo-email-dark.png",
121
+ },
122
+ },
123
+ },
124
+ });
125
+ ```
126
+
127
+ The loader hands every layer's `app/app.config.ts` to the email renderer too (`@antelopejs/dms-frontend` 0.6.0 or later), merged in module priority order, so a module registered with a priority above the DMS's (`0`) wins. `dark` is optional: when it passes the same check and differs from `light`, the header becomes a `<picture>` whose dark source the mail clients that follow the reader's dark mode pick, and the others draw `light`.
128
+
129
+ ## Replacing a built-in email
130
+
131
+ The render server looks a template up by its file name, in module priority order, and renders the first match. A module registered with a `priority` above the DMS's (`0`, see [`AddFrontendModule`](/docs/interfaces/core/pages-and-modules#addfrontendmoduleoptions)) therefore replaces a built-in email by shipping a component of the same name in its `dms.email.ts`:
132
+
133
+ ```ts [frontend-vue/dms.email.ts]
134
+ export const serverEmailTemplates = import.meta.glob("./app/emails/**/*.vue");
135
+ ```
136
+
137
+ ```vue [frontend-vue/app/emails/EmailResetPassword.vue]
138
+ <script setup lang="ts">
139
+ import { useI18n } from "vue-i18n";
140
+ import EmailLayout from "#dms-layout/app/emails/components/EmailLayout.vue";
141
+ import EmailOTP from "#dms-layout/app/emails/components/EmailOTP.vue";
142
+
143
+ defineProps<{ userName: string; resetCode: string; expiresInHours: number }>();
144
+
145
+ const { t, locale } = useI18n();
146
+ </script>
147
+
148
+ <template>
149
+ <EmailLayout :title="t('emails.reset_password.title')" :lang="locale" public-logo-only>
150
+ <EText>{{ t("emails.common.greeting", { name: userName }) }}</EText>
151
+ <EText>Use this code to choose a new Acme password:</EText>
152
+ <EmailOTP :code="resetCode" :expires-in="t('emails.common.hours', expiresInHours)" />
153
+ </EmailLayout>
154
+ </template>
155
+ ```
156
+
157
+ The replacement receives the props listed in [Props](#props) and keeps the backend's subject. Do not register the name again with `RegisterHtmlTemplate`: the DMS already sends it. At equal priority the first module by id wins, so set the priority explicitly rather than relying on it.
Binary file