Email
Channels

Email

Configure and send typed emails

The email channel is provided by @betternotify/email. It defines the slots, validation, and rendering logic for email notifications — subject, template, from address, reply-to, tags, and priority.

When to use email

Email is the right channel when the recipient needs a persistent, rich-content notification they can read on their own time. Use it for transactional messages (welcome emails, password resets, order confirmations), marketing campaigns, invoices, reports, or anything where the content benefits from HTML formatting, attachments, or a paper trail the recipient can search later.

npm install @betternotify/email

Setup

Create the channel with optional defaults that apply to every email procedure:

import { createNotify } from '@betternotify/core';
import { emailChannel } from '@betternotify/email';

const email = emailChannel({
  defaults: {
    from: { name: 'My App', email: 'hello@myapp.com' },
    replyTo: 'support@myapp.com',
    headers: { 'X-App': 'myapp' },
  },
});

const rpc = createNotify({ channels: { email } });

Channel defaults

Prop

Type

Slots

These are the builder methods available on rpc.email():

Prop

Type

Send arguments

When calling .send(), these are the arguments the caller provides:

Prop

Type

Full example

import { createNotify, createClient } from '@betternotify/core';
import { emailChannel } from '@betternotify/email';
import { smtpTransport } from '@betternotify/smtp';
import { z } from 'zod';

const email = emailChannel({
  defaults: { from: { name: 'Acme', email: 'noreply@acme.com' } },
});

const rpc = createNotify({ channels: { email } });

const catalog = rpc.catalog({
  welcome: rpc
    .email()
    .input(z.object({ name: z.string(), verifyUrl: z.string().url() }))
    .subject(({ input }) => `Welcome, ${input.name}!`)
    .template({
      render: async ({ input }) => ({
        html: `<h1>Welcome, ${input.name}!</h1><a href="${input.verifyUrl}">Verify</a>`,
        text: `Welcome, ${input.name}! Verify: ${input.verifyUrl}`,
      }),
    }),
});

const mail = createClient({
  catalog,
  transportsByChannel: {
    email: smtpTransport({ host: 'smtp.gmail.com', port: 587, auth: { user: '...', pass: '...' } }),
  },
});

await mail.welcome.send({
  to: 'ada@example.com',
  input: { name: 'Ada', verifyUrl: 'https://acme.com/verify?token=abc' },
});

Templates

The template slot accepts either a TemplateAdapter object or a plain function. For React Email, MJML, or Handlebars templates, see Templates.

// Plain function
.template(async ({ input }) => ({
  html: `<p>Hello ${input.name}</p>`,
  text: `Hello ${input.name}`,
}))

// TemplateAdapter object
.template({
  render: async ({ input }) => ({
    html: `<p>Hello ${input.name}</p>`,
    text: `Hello ${input.name}`,
  }),
})

The template can also return a subject field to override the .subject() slot — useful when the template adapter controls the full email.

Address format

Addresses accept a string or an object with name and email:

// String
await mail.welcome.send({ to: 'ada@example.com', input });

// Object with display name
await mail.welcome.send({
  to: { name: 'Ada Lovelace', email: 'ada@example.com' },
  input,
});

// Array of recipients
await mail.welcome.send({
  to: [
    'ada@example.com',
    { name: 'Charles Babbage', email: 'charles@example.com' },
  ],
  input,
});

On this page