Github
Registry

Email

Send registry invitations and password resets through SMTP or a custom email module.

The registry sends invitation and password-reset email once email.from is set and the provider's required options have values. Without email, an administrator sees each invitation link once and delivers it by hand, and self-service password reset is not available.

handoff.config.registry.ts
runtime: {
  registry: {
    email: {
      from: 'Handoff <no-reply@example.com>',
      provider: 'smtp',
      options: {
        host: fromEnv('SMTP_HOST'),
        port: 465,
        user: fromEnv('SMTP_USER'),
        password: fromEnv('SMTP_PASSWORD'),
      },
    },
  },
},
  • provider is required when you set email: smtp or custom.
  • smtp works with any SMTP server, for example Amazon SES, SendGrid, or Microsoft 365. Port 465 (the default) uses TLS; 587 uses STARTTLS.
  • password and apiKey must use fromEnv().
  • A profile can set a different sender or provider. When a profile changes the provider, it replaces options.
  • The build bakes the provider choice. See Build-time and runtime values.

Custom provider

custom names a server module that default-exports defineEmailProvider(). Use it for an HTTP API.

handoff.config.registry.ts
email: {
  from: 'Handoff <no-reply@example.com>',
  provider: 'custom',
  module: './server/email/resend.mjs',
  options: { apiKey: fromEnv('RESEND_API_KEY') },
},
server/email/resend.mjs
import { defineEmailProvider } from 'handoff-app/providers'

export default defineEmailProvider(({ options }) => ({
  async send({ from, to, subject, html, text }) {
    if (!options.apiKey) throw new Error('RESEND_API_KEY is not set.')
    const response = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: { Authorization: `Bearer ${options.apiKey}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ from, to: [to], subject, html, text }),
    })
    if (!response.ok) {
      throw new Error(`Resend rejected the message: ${response.status} ${await response.text()}`)
    }
  },
}))
  • send must throw when the provider does not accept the message.
  • The registry does not check a custom module's options. If the key is empty, every email fails.

On this page