---
title: "Templates"
description: "The emails the package sends (the one-time code, welcome, delete-account, invite, gift), how to restyle any of them in one place without replacing the transport, and how to preview them."
canonical_url: "https://nuxt-backend.dev/docs/email/templates"
---
# Templates

> The emails the package sends (the one-time code, welcome, delete-account, invite, gift), how to restyle any of them in one place without replacing the transport, and how to preview them.

Every email the package sends on your behalf comes from a template that returns `{ to, subject, html?, text? }`, an `EmailMessage`. The templates live in one place, the `setupEmail` instance in `backend/email.ts`: `setupAuth` and `setupBilling` take that instance as their `email` option and send through it, so one override restyles an email wherever it goes out. The defaults are minimal, dependency-free HTML and text.

## The templates

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      Sent when
    </th>
    
    <th>
      Data
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        otp
      </code>
    </td>
    
    <td>
      A one-time code: to sign in, to verify an address (<code>
        email-verification
      </code>
      
      , which with <code>
        reason: 'email-change'
      </code>
      
       confirms the current address at the start of an email change, in its own words), and for the new address of an email change (<code>
        change-email
      </code>
      
      )
    </td>
    
    <td>
      <code>
        { email, otp, type: 'sign-in' | 'email-verification' | 'forget-password' | 'change-email', reason?: 'email-change' }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        emailChanged
      </code>
    </td>
    
    <td>
      An email change went through: to the old address, so whoever holds it hears of a change they didn't make
    </td>
    
    <td>
      <code>
        { email, newEmail }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        welcome
      </code>
    </td>
    
    <td>
      Once, right after a user is created
    </td>
    
    <td>
      <code>
        { email, name }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        deleteAccount
      </code>
    </td>
    
    <td>
      Confirmation link for account deletion
    </td>
    
    <td>
      <code>
        { email, url }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        invite
      </code>
    </td>
    
    <td>
      Workspace invitation with an accept link
    </td>
    
    <td>
      <code>
        { email, url, inviterName, inviterEmail, organizationName, role }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gift
      </code>
    </td>
    
    <td>
      A gift is paid, to its recipient (see <a href="https://nuxt-backend.dev/docs/billing/credits/gifts#the-notification-email">
        Gifts
      </a>
      
      )
    </td>
    
    <td>
      <code>
        { recipientEmail, purchaserName?, item, claimUrl }
      </code>
      
      : the sender's name (at most 40 characters, no links), the pack's name and the app's URL, never the purchaser's email or message
    </td>
  </tr>
</tbody>
</table>

Each template also gets a render context:

- `t(key, params?)` reads the package's labels, the words the defaults are made of: `t('email.invite.subject', { workspace: 'Acme' })` is `Join Acme`.
- `locale` is the recipient's locale, `'en'` until accounts carry one.
- `brand` is `{ name, url }`: `url` is `SITE_URL`, and `name` is `setupEmail`'s `brand.name`, or the site's host without one. Every packaged email signs off with the name, linked to `url`.

## Overriding

Pass any subset as `templates` on `setupEmail`. The transport stays the component's; only the message changes. Each override gets the data, the render context, and `byDefault`, the packaged template bound to the same context:

```ts [backend/email.ts]
import { setupEmail } from 'nuxt-backend/email'
import { components } from './_generated/api'

export const email = setupEmail(components, {
  brand: { name: 'Acme' },
  templates: {
    otp: ({ email, otp, type }) => ({
      to: email,
      subject: type === 'sign-in' ? `${otp} is your Acme sign-in code` : 'Your Acme verification code',
      text: `Your code: ${otp}. It expires in 5 minutes.`,
      html: renderOtpEmail({ otp }),   // your own renderer
    }),
    invite: (data, { byDefault }) => ({
      ...byDefault(data),    // keep the default body…
      subject: `${data.inviterName} wants you on ${data.organizationName}`,   // …change the subject
    }),
  },
})
```

Render however you like: a string builder, `@vue-email`, `@react-email`, anything that returns HTML and text. An override may be `async`, so a renderer that returns a promise works as is; `byDefault` stays synchronous. The [advanced example](https://nuxt-backend.dev/docs/start/templates#example-advanced) overrides `otp`, `invite` and `gift` this way.

Some fields hold text a user typed: a name, a workspace name, a gift sender's name. Escape them when you write your own HTML, or that user can put markup into an email your domain sends. `escapeHtml` from `nuxt-backend/email` is the one the packaged templates use:

```ts
import { escapeHtml } from 'nuxt-backend/email'

gift: (data, { byDefault }) => ({
  ...byDefault(data),
  html: `<p>From ${escapeHtml(data.purchaserName ?? 'someone')}</p>`,
}),
```

<note>

`backend/auth.ts` imports `email` from `backend/email.ts`, so `email.ts` must not import `auth.ts` back: the import cycle leaves `email` undefined while `setupAuth` runs. To read the signed-in user in `email.ts`, use `ctx.auth.getUserIdentity()`.

</note>

### The welcome email

`welcome` goes out once, right after signup, on every app. Onboarding that follows it belongs in an [email sequence](https://nuxt-backend.dev/docs/email/engagement/sequences): the scaffold's `onSignup` sequence starts three days later. Restyle the welcome itself through `templates.welcome`.

### Taking over the invitation

The workspace plugin's own `organization.sendInvitationEmail` on `setupAuth` replaces the invitation email entirely, delivery included. It bypasses the component's delivery tracking.

## Sent once

The welcome email, the email-change notice and a gift notification carry an idempotency key under the package's own prefix: `nuxt-backend:welcome:<user>`, `nuxt-backend:email-changed:<user>:<address>` and `nuxt-backend:gift:<id>`. While the first email is on record (finalized records are pruned after a week), a second send with the same key returns its id and delivers nothing, so a retried request or a redelivered webhook doesn't email twice. An invitation carries none: every send of it, a resend included, goes out. Your own sends take the same option, `email.send(ctx, { …, idempotencyKey })`; keep the `nuxt-backend:` prefix for the package's.

## Previewing

`email.render(name, data)` resolves a template in effect, override included, without sending it. The playground's email-templates page renders each one with sample data:

```ts [backend/emailTemplates.ts]
import { query } from './_generated/server'
import { email } from './email'

export const otpPreview = query({
  args: {},
  // { to, subject, html, text }
  handler: () => email.render('otp', { email: 'you@example.com', otp: '482913', type: 'sign-in' }),
})
```

`email.sendTemplate(ctx, name, data, { idempotencyKey? })` renders and sends in one call, the way auth and billing do.

<page-keys>

- Try it live in the playground: [/playground/platform/email-templates](https://nuxt-backend.dev/playground/platform/email-templates)
- API reference: [/docs/reference/api/email](https://nuxt-backend.dev/docs/reference/api/email)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/email-templates.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/email-templates.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
