AuthResendChannel
WIPResend.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.
With magic-link
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 error | duck-auth error |
|---|---|
| 401 Unauthorized | AUTH/PROVIDER_FAILED (check the API key) |
| 422 Unprocessable | AUTH/PROVIDER_FAILED (template / from-address issue) |
| 429 Too Many Requests | AUTH/RATE_LIMITED (Resend account rate limit) |
| 5xx | AUTH/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 })
})