Registry
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.
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'),
},
},
},
},provideris required when you setemail:smtporcustom.smtpworks with any SMTP server, for example Amazon SES, SendGrid, or Microsoft 365. Port 465 (the default) uses TLS; 587 uses STARTTLS.passwordandapiKeymust usefromEnv().- 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.
email: {
from: 'Handoff <no-reply@example.com>',
provider: 'custom',
module: './server/email/resend.mjs',
options: { apiKey: fromEnv('RESEND_API_KEY') },
},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()}`)
}
},
}))sendmust 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.