Skip to main content

Channels

WIP

Outbound message delivery - email, SMS, WebPush. Console + Test for dev, SMTP + Resend + SES + Twilio + WebPush for production.

What is a channel?

A channel is an outbound transport for messages - magic links, verification emails, password resets, MFA codes, anomaly alerts. Every channel implements AuthChannel.IChannel:

interface IChannel {
  id: string
  kind: 'email' | 'sms' | 'webpush'
  send(input: {
    templateId: string
    identity: { id: string }
    tenant: { tenantId?: string }
    vars: Record<string, unknown>
  }): Promise<{ ok: boolean; providerMessageId: string }>
}

Wire channels into providers that need outbound delivery (most notably magic-link):

import { magicLink } from '@gentleduck/auth/providers/magic-link'
import { AuthResendChannel } from '@gentleduck/auth/channels/resend'
import { AuthTwilioChannel } from '@gentleduck/auth/channels/twilio'

magicLink({
  channels: {
    email: new AuthResendChannel({ apiKey: ..., from: 'no-reply@example.com' }),
    sms: new AuthTwilioChannel({ accountSid, authToken, from }),
  },
  // ...
})

Shipped channels

ChannelImportKind
AuthConsoleChannel@gentleduck/auth/channels/consoleemail / sms / webpush - dev
AuthSmtpChannel@gentleduck/auth/channels/smtpemail - any nodemailer-compatible SMTP relay
AuthResendChannel@gentleduck/auth/channels/resendemail - Resend transactional API
AuthSesChannel@gentleduck/auth/channels/sesemail - AWS SES
AuthTwilioChannel@gentleduck/auth/channels/twiliosms - Twilio Programmable Messaging
AuthWebPushChannel@gentleduck/auth/channels/webpushwebpush - Web Push protocol with VAPID
AuthTestChannel@gentleduck/auth/channels/consolecaptures sends into an outbox array - for tests
AuthNoopChannel@gentleduck/auth/channels/consolediscards sends, always returns ok - for benchmarks

Templating

duck-auth does not template messages itself. The channel receives a typed vars payload from the runtime; rendering the body is your channel's responsibility:

class CustomResendChannel implements AuthChannel.IChannel {
  id = 'custom-resend'
  kind = 'email' as const

  async send({ templateId, identity, vars }) {
    const html = mjml(`<mjml>...${vars.url}...</mjml>`).html
    const res = await resend.emails.send({
      from: 'no-reply@example.com',
      to: vars.email as string,
      subject: this.subjectFor(templateId, vars),
      html,
    })
    return { ok: true, providerMessageId: res.id }
  }

  subjectFor(templateId: string, vars: Record<string, unknown>) {
    switch (templateId) {
      case 'magic-link': return `Sign in to ${vars.appName}`
      case 'password-reset': return `Reset your ${vars.appName} password`
      case 'email-verify': return `Verify your email`
      default: return 'Notification'
    }
  }
}

For most apps, the shipped channels with their default templates are enough; for full control, implement your own.

magicLink({
  channels: {
    email: new AuthResendChannel({ ... }),
    sms: new AuthTwilioChannel({ ... }),
    webpush: new AuthWebPushChannel({ ... }),
  },
})

// Client picks one:
await client.beginProvider('magic-link', { email, channel: 'email' })
await client.beginProvider('magic-link', { email, channel: 'sms' })
await client.beginProvider('magic-link', { email, channel: 'webpush' })

The provider routes to channels[channel].send(...). If a channel fails (transient SMTP error), the provider raises AUTH/PROVIDER_FAILED and the user can retry.

Audit trail

Every channel call emits a channel.sent (or channel.failed) event on the configured event bus:

auth.events.on('channel.sent', (event) => {
  console.log('sent', event.templateId, 'to', event.identity.id)
})

HIPAA and SOC2 compliance presets require the audit event to be persisted; they automatically wire a webhook deliverer to ship every channel event to your audit log.