eusend
Frameworks

Auth.js (NextAuth)

Send Auth.js magic-link sign-in email through eusend, over SMTP with the Nodemailer provider or over HTTP with a custom provider.

Auth.js sends one kind of email: the magic link for passwordless sign-in. It can send it through any SMTP server or through a function you write, and eusend works with both. Use SMTP if you want the least code; use the HTTP provider if you want your own template, or your runtime has no TCP sockets.

Either way Auth.js needs a database adapter to store the verification token, and eusend needs a verified domain for the from address.

.env.local
EUSEND_API_KEY=eu_live_...
EMAIL_FROM="Acme <auth@yourdomain.com>"

Option 1 — SMTP with the Nodemailer provider

npm install nodemailer
auth.ts
import NextAuth from 'next-auth'
import Nodemailer from 'next-auth/providers/nodemailer'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter, // your database adapter
  providers: [
    Nodemailer({
      server: {
        host: 'smtp.eusend.dev',
        port: 465,
        secure: true,
        auth: { user: 'eusend', pass: process.env.EUSEND_API_KEY },
      },
      from: process.env.EMAIL_FROM,
    }),
  ],
})

The username is always the literal word eusend; the API key is the password. Port 465 uses implicit TLS, so secure must be true — eusend does not offer STARTTLS on 587.

Option 2 — HTTP with a custom provider

Auth.js accepts a provider of type email with your own sendVerificationRequest. This one calls the eusend API with fetch, so it runs anywhere, including edge runtimes.

auth.ts
import NextAuth from 'next-auth'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter, // your database adapter
  providers: [
    {
      id: 'http-email',
      name: 'Email',
      type: 'email',
      maxAge: 60 * 60 * 24,
      async sendVerificationRequest({ identifier: to, url }) {
        const { host } = new URL(url)

        const res = await fetch('https://api.eusend.dev/emails', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${process.env.EUSEND_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            from: process.env.EMAIL_FROM,
            to,
            subject: `Sign in to ${host}`,
            html: `<p><a href="${url}">Sign in to ${host}</a></p><p>If you did not request this, you can ignore this email.</p>`,
            text: `Sign in to ${host}\n${url}\n\nIf you did not request this, you can ignore this email.`,
            track_clicks: false,
          }),
        })

        if (!res.ok) {
          const { code } = await res.json()
          throw new Error(`eusend refused the sign-in email: ${code}`)
        }
      },
    },
  ],
})

Then start the flow with the provider's id: signIn('http-email', { email }).

⚠

Keep click tracking off for magic links, as above. Tracking rewrites the link through a redirect, and a sign-in link should go straight to your app.

Why a sign-in email fails

Auth.js surfaces a thrown error as a generic sign-in failure, so check the email log first. The usual causes:

  • DOMAIN_NOT_VERIFIED — the from domain has not finished verifying.
  • ALL_SUPPRESSED — the address hard-bounced or complained before and is on your suppression list.
  • DAILY_LIMIT_EXCEEDED — the free plan's daily cap. A sign-in flow should not be the place you find out, so watch your usage on the dashboard.

See also

  • Next.js for the rest of your app's email.
  • Better Auth, which has an official eusend plugin with default templates.