How to Customize Magic Link Emails from Passwordless Authentication in NextAuth


Sending magic links with passwordless authentication with NextAuth is quite simple. With emails relayed through an SMTP server, the next step is to customize those emails.

NextAuth has a default email template that we can modify and style to our liking.

The first step is to override the sendVerificationRequest function with our own custom request.

The code snippets in this article require NextAuth.js v4. Check out how to upgrade to version 4.

// pages/api/auth/[...nextauth].js
import EmailProvider from `next-auth/providers/email`;
export default NextAuth({
  ...
  providers: [
    EmailProvider({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
      sendVerificationRequest: customVerificationRequest,
    }),
    ...
  ];
});

We’ll be using the default sendVerificationRequest function, but replacing the html() function, which holds the email’s HTML body, with our own.

Whether we define this custom function inside pages/api/auth/[...nextauth].js or in another utility file is up to you.

import nodemailer from "nodemailer";
const customVerificationRequest = ({
  identifier: email, url, token, baseUrl, provider
}) => {
  return new Promise((resolve, reject) => {
    const { server, from } = provider;
    const site = baseUrl.replace(/^https?:\/\//, "");
    nodemailer.createTransport(server).sendMail({
      to: email,
      from,
      subject: `Sign in to ${site}`,
      text: text({ url, site, email }),
      html: html({ url, site, email }),
    }, (error) => {
      if (error) {
        logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error);
        return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error));
      }
      return resolve();
    });
  });
};

Then, we can define our custom html() function here. There are some default styles defined in the beginning. As we can see, the function returns a stringified DOM tree.

const html = ({ url, site, email }) => {
  const escapedEmail = `${email.replace(/\./g, "​.")}`;
  const escapedSite = `${site.replace(/\./g, "​.")}`;
  const backgroundColor = "#f9f9f9";
  const textColor = "#444444";
  const mainBackgroundColor = "#ffffff";
  const buttonBackgroundColor = "#346df1";
  const buttonBorderColor = "#346df1";
  const buttonTextColor = "#ffffff";
  return `
    <body style="background: ${backgroundColor};">
      <table width="100%" border="0" cellspacing="0" cellpadding="0">
        <tr>
          <td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
            <strong>${escapedSite}</strong>
          </td>
        </tr>
      </table>
      <table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
        <tr>
          <td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
            Sign in as <strong>${escapedEmail}</strong>
          </td>
        </tr>
        <tr>
          <td align="center" style="padding: 20px 0;">
            <table border="0" cellspacing="0" cellpadding="0">
              <tr>
                <td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
              </tr>
            </table>
          </td>
        </tr>
        <tr>
          <td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
            If you did not request this email you can safely ignore it.
          </td>
        </tr>
      </table>
    </body>`;
};

Finally, we can also modify the text() function, which serves as a fallback for email clients that don’t render HTML.

const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`;