Skip to main content

AuthResendChannel

WIP

Resend.com transactional email channel.

Install

bun add resend

Setup

import { AuthResendChannel } from '@gentleduck/auth/channels/resend'

const channel = new AuthResendChannel({
  apiKey: process.env.RESEND_API_KEY!,
  from: 'no-reply@example.com',
  id: 'resend',
})

Make sure the from address is on a verified domain in your Resend dashboard - Resend rejects unverified domains.

auth.providers.use(
  magicLink({
    channels: { email: channel },
    findIdentityByEmail: (email) => auth.identities.findByEmail(email),
  }),
)

Templating

For branded emails, subclass AuthResendChannel and override renderMessage() to produce the HTML / text body:

class BrandedResendChannel extends AuthResendChannel {
  async renderMessage({ templateId, vars }) {
    switch (templateId) {
      case 'magic-link':
        return {
          subject: `Sign in to ${vars.appName}`,
          html: renderMjml('magic-link.mjml', vars),
          text: `Sign in: ${vars.url}`,
        }
      case 'password-reset':
        return { /* ... */ }
      // ...
    }
  }
}

Or use Resend's React Email library to compose templates with React components.

Error mapping

Resend errorduck-auth error
401 UnauthorizedAUTH/PROVIDER_FAILED (check the API key)
422 UnprocessableAUTH/PROVIDER_FAILED (template / from-address issue)
429 Too Many RequestsAUTH/RATE_LIMITED (Resend account rate limit)
5xxAUTH/PROVIDER_FAILED (transient - client should retry)

Webhook integration

Resend sends bounce / complaint webhooks. Wire them into your auth state:

app.post('/webhooks/resend', async (req, res) => {
  const event = req.body  // verify signature first!
  if (event.type === 'email.bounced') {
    const identity = await auth.identities.findByEmail(event.data.email)
    if (identity) {
      await auth.identities.markEmailUndeliverable(identity.id, { reason: 'bounce' })
    }
  }
  res.json({ ok: true })
})