← All case studies Mailgun → Resend

Four ShipFast domains left Mailgun for Resend and stopped forwarding replies

Bill Nalen moved four ShipFast domains off Mailgun in December 2023. Magic links stay on NextAuth. The mg subdomain goes away. In his write-up Resend does not forward replies, and each domain ends up with its own Resend account.

Bill Nalen published this on 3 December 2023, as a side step from his ShipFast tutorials. It is his own write-up. He had already run the change on four domains, and he says each one takes him about fifteen minutes.

ShipFast’s default mail path is Mailgun. NextAuth sends magic links through nodemailer, and Mailgun routes can forward mail that arrives for the domain. The template does both with MX records on a subdomain, which is why config.js puts mg in front of the address.

He could point other messages at a normal address in config.js. The magic link still came from the mg address, because NextAuth sends that mail through the Mailgun setup. The message that lets someone into the app was the one carrying the subdomain.

The day before the post, the route cap got in the way. On Mailgun’s pay-as-you-go plan he had five routes per account. He was planning more Small Bets, and he had used the five. Another Mailgun account would have covered five more domains. The Foundation plan was $35 a month for 50,000 emails. Marc, in the ShipFast community, had mentioned Resend.

The prices he prints for that month put Mailgun’s flex plan at $0 for 1,000 emails and Foundation at $35 for 50,000. Resend’s free plan is $0 for 3,000 emails. Pro is $20 for 50,000. He does not say which Resend plan the four domains landed on. The Mailgun → Resend playbook says to check the current Mailgun plan before you treat five routes as today’s cap. Five is what he published for pay-as-you-go in 2023.

An inbox on the domain, then one variable

Resend’s signup, as he did it, wants an address on the domain you are about to send from. At Porkbun, his registrar, a mailbox is $24 a year. Forwarding that domain address to another inbox is free, and he says the forward is enough to receive the signup mail. Resend then sends its own magic link, and you land on the overview page.

He creates an API key and stores it in .env.local as RESEND_API_KEY. Before he changes the app, he uses the Send Email button on that overview page, to see that the key can send at all.

nodemailer already reads EMAIL_SERVER, so the magic-link path can stay on that variable. On Mailgun the value was Mailgun’s SMTP server, plus a password. Resend uses the API key as the SMTP secret, so there is no second password to create.

EMAIL_SERVER=smtp://resend:[email protected]:587

The key sits between resend: and @. The host is smtp.resend.com, port 587. That string is his December 2023 setup. The playbook says to confirm the current SMTP host before you copy it into another app.

The config object is still called mailgun

He leaves the key name alone and edits five fields under mailgun in config.js.

subdomain becomes an empty string. In this setup Resend does not send through an mg host, so the prefix has nothing to attach to.

fromNoReply is the From line on the magic link. He says the address can be one that does not exist. He uses a noreply address because he does not want those replies in an inbox. If someone hits reply and the mailbox is not real, the reply goes nowhere, which is what he wants on login mail.

fromAdmin is the From line on every other message the app sends. Those go through the sendEmail helper that used to live in the Mailgun library.

supportEmail shows up in ButtonSupport.js and Footer.js. He is the only person on these projects, so he keeps it the same as his main address.

forwardRepliesTo he still fills in, with that same address. In this post he says Resend only sends, so that address is stored and never used.

The playbook says Resend can receive on a domain you configure, and that Mailgun’s route actions still have to be rebuilt by hand. He left receiving off. If you still want the replies those routes used to catch, you need a receiving setup this write-up never builds.

He also never exports bounces, complaints, or unsubscribes. The playbook treats that file as part of the backup, because a Resend suppression applies to the whole team, on every domain. His tutorial skips the export.

Six Mailgun records out, three Resend records in

On Resend’s Domains tab he adds the naked domain, with no prefix. Resend then shows three DNS records. He names one of them: an MX record on a subdomain called send.

That MX is how the From address can drop mg while the domain’s own inbox stays put. Mailgun had wanted its MX on mg, which is how the subdomain leaked into the address people saw. Resend’s MX sits on send. He says the domain’s existing MX records keep receiving mail. The product still does not forward those messages into the app. A Porkbun mailbox, or the free forward, is what can still accept them.

At Porkbun he deletes the six Mailgun records if they are still there, then pastes Resend’s three in as shown. He says to copy them with no extra mg label. Back on Resend he clicks Verify DNS Records. Pending should turn green. He says that can take a few minutes, and if it stays pending the records are wrong.

The playbook wants Resend’s records added first, and Mailgun’s SPF include removed only after the app has stopped sending through Mailgun. He deletes the six Mailgun records and then adds Resend’s three. He never mentions a failed magic link in that gap, or Mailgun still being able to send once those records were gone. The playbook keeps both sets until the app has stopped calling Mailgun. His write-up goes straight from the old records to the new ones.

The same two variables, locally and on Vercel

He starts the app with npm run dev. If you are logged in, log out and sign up again. The button in the template says Sign in with Email. The message should come from the noreply address, and the link in it should open the local app.

Production is those same two values, copied from .env.local into Vercel: RESEND_API_KEY and EMAIL_SERVER. Then he commits config.js and syncs to GitHub, which starts the Vercel build.

If the app only sends magic links, he says you can stop once EMAIL_SERVER points at Resend. The resend package is for every other message the app sends.

The helper, when something else sends

He adds libs/resend.js. The function takes to, subject, text, html, and replyTo, same as the old helper. from still reads config.mailgun.fromAdmin. The object name survives because he never renamed the config block, and the new file reaches into it.

import { Resend } from 'resend';
import config from '@/config';

const resend = new Resend(process.env.RESEND_API_KEY);

export const sendEmail = async ({ to, subject, text, html, replyTo }) => {
  await resend.emails.send({
    from: config.mailgun.fromAdmin,
    to: to,
    reply_to: replyTo,
    subject: subject,
    text: text,
    html: html,
  });
};

Install is npm install resend. Call sites change the import and leave the arguments alone. In the post the old path is written @/libs/maligun.js. Earlier in the same post he calls the file Mailgun.js. Use the name that is actually in the template.

reply_to is the field he passes through. A reply to fromAdmin can land when that address is a real mailbox. A reply to the noreply magic link still has nowhere to go, which is the behavior he wanted on login mail.

The post also links Supabase SMTP docs that salo423 and Khalizo suggested in Discord. Those docs are for Supabase Auth, and the four domains in the write-up run NextAuth, nodemailer, and Vercel.

Four accounts, and replies that no longer forward

He has run the change on four domains, at about fifteen minutes each. The magic link shows the plain domain, and the Mailgun route cap is no longer in the path.

The drawback he names is one Resend account per domain. The signup he describes uses an address on that domain, so four domains became four accounts. He calls that a tradeoff he can live with. forwardRepliesTo is still in config.js, filled in, and unused, because this cutover never rebuilds the routes that used to forward replies.