Dyrecteddyrected
Guides

Sending Email

Configure transactional email, customise built-in templates, and send emails from hooks.

Dyrected sends transactional emails automatically for auth events — welcome, invite, password reset, and password changed. You wire in your own email provider via a send callback; Dyrected never cares which library or service you use.


1. Wire in your email provider

Email runs through a single send callback in your config. Dyrected hands it the recipient, subject, and rendered HTML; you forward those to whatever provider you use. Here it is with Resend, a solid default:

// dyrected.config.ts
import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export default defineConfig({
  email: {
    from: '[email protected]',
    send: async ({ to, subject, html }) => {
      await resend.emails.send({ from: '[email protected]', to, subject, html })
    },
  },
})

Every auth email now flows through Resend. Prefer a different provider? The send signature never changes — swap the body for the one you want.

Nodemailer (SMTP)

Nodemailer is the usual choice when you send through your own SMTP server. Create a transport once and call it from send:

import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
})

export default defineConfig({
  email: {
    from: '[email protected]',
    send: async ({ to, subject, html }) => {
      await transporter.sendMail({ from: '[email protected]', to, subject, html })
    },
  },
})

Any library that sends email works — Postmark, SendGrid, AWS SES, etc.


2. Development — Ethereal fallback

If email is not configured and NODE_ENV is not production, Dyrected automatically routes emails through Ethereal — a free fake SMTP service that captures outgoing emails without delivering them. No account or setup required.

On first send, credentials and a preview URL are printed to the console:

[dyrected/core] No email config — using Ethereal for dev email preview.
[dyrected/core] Ethereal login: https://ethereal.email  user: [email protected]  pass: xxx
[dyrected/core] Email preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...

Click the preview URL to see the rendered email.


3. Built-in email events

Four auth events trigger automatic emails. All are best-effort — a failed send is logged but never blocks the API response.

EventTriggerTemplate key
Account createdPOST /{slug}/first-user or POST /{slug}/accept-invitewelcome
Invitation sentPOST /{slug}/inviteinvite
Password reset requestedPOST /{slug}/forgot-passwordresetPassword
Password successfully changedPOST /{slug}/reset-passwordpasswordChanged

4. Customise built-in templates

Override the HTML (and optionally the subject) for any built-in email by adding a templates object. Each key is a function that receives relevant data and returns { html, subject? }. Omitting subject keeps the default.

export default defineConfig({
  email: {
    from: '[email protected]',
    send: async ({ to, subject, html }) => {
      await resend.emails.send({ from: '[email protected]', to, subject, html })
    },
    templates: {
      welcome: ({ email }) => ({
        subject: 'Welcome to Acme',
        html: `<h1>Welcome!</h1><p>Your account is ready: ${email}</p>`,
      }),
      invite: ({ token, invitedByEmail }) => ({
        subject: "You've been invited to Acme",
        html: `
          <p>Invited by <strong>${invitedByEmail}</strong>.</p>
          <p>Your invite token:</p>
          <pre>${token}</pre>
        `,
      }),
      resetPassword: ({ token, url }) => ({
        subject: 'Reset your Acme password',
        html: url
          ? `<p><a href="${url}">Reset your password</a> (expires in 1 hour)</p>`
          : `<p>Your reset token (expires in 1 hour):</p><pre>${token}</pre>`,
      }),
      passwordChanged: ({ email }) => ({
        subject: 'Your Acme password was changed',
        html: `<p>The password for <strong>${email}</strong> was just changed.</p>`,
      }),
    },
  },
})

Template args reference

TemplateArgs
welcome{ email: string }
invite{ token: string, invitedByEmail?: string }
resetPassword{ token: string, url?: string }url is the full reset link when resetUrl was passed to forgot-password
passwordChanged{ email: string }

5. Send custom emails from hooks

For emails beyond the four built-in events — order confirmations, notifications, etc. — use an afterChange hook with your email library directly:

import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export default defineConfig({
  collections: [
    {
      slug: 'orders',
      hooks: {
        afterChange: [
          async ({ doc, operation }) => {
            if (operation === 'create') {
              await resend.emails.send({
                from: '[email protected]',
                to: doc.customerEmail,
                subject: `Order #${doc.id} confirmed`,
                html: `<p>Thanks for your order!</p>`,
              })
            }
          },
        ],
      },
      fields: [
        { name: 'customerEmail', label: 'Customer Email', type: 'email', required: true },
      ],
    },
  ],
})

On this page