You can move in three steps, and each one ships on its own. Stop after any of them.
- Swap the SDK, keep Resend. Replace
resend.emails.send()withmail()and set the provider toresend. Your mail still goes out through your Resend account, on your Resend plan, from your Resend domain. Nothing about delivery changes. - Move sending to Postboi.
bunx postboi migrate resendbrings your domains, audiences and webhooks across, then you change one line of config. Yourmail()calls stay the same. - Retire Resend. Check your webhooks, carry your suppressions over, and take Resend’s DNS records down once nothing sends through it.
If you’re starting fresh rather than moving, Quick start is quicker.
1. Swap the SDK, keep Resend
Install Postboi and tell it to use Resend. bunx postboi init asks which provider you
send with: pick bring your own provider, then Resend, and it asks for your Resend
API key and writes the config. By hand it’s one file:
RESEND_API_KEY stays in your environment, where it already is.
Then change the sends. Before:
After:
from comes from the config default, and the plain-text part is written from body for
you, as Resend does from html.
Errors throw. Resend’s Node SDK hands back { data, error } and never throws. mail() returns { id } on success and throws a PostboiError on failure, with the
provider’s own error name in code. If you checked error after every send, that becomes a try/catch, or nothing at all where a failed send should fail the request anyway.
Field by field
A batch where everyone gets the same email with a few values swapped is shorter than Resend’s, and on Resend it still goes out as one request to its batch endpoint:
React Email
Resend takes a React element in react. Postboi takes HTML, so render it first with
React Email’s own render(). The template
doesn’t change:
body takes the promise render() returns, so there’s no need to await it.
2. Move sending to Postboi
Once your app sends through mail(), moving off Resend is configuration.
Get a Postboi account. Run bunx postboi init and pick Postboi. It signs you in and
writes a POSTBOI_TOKEN.
Bring your Resend account across. One command reads your Resend account with your RESEND_API_KEY and recreates what Postboi can hold:
- Domains are added to Postboi and their DNS records printed. Publish the three DKIM
CNAME records. They sit beside Resend’s rather than replacing them: Resend signs with
resend._domainkeyand uses asend.subdomain for bounces, while Postboi uses its own DKIM names and abounce.subdomain. Both can be live at once, so nothing you send through Resend is affected while the domain verifies. Your DMARC record stays as it is. - Audiences become lists. Contacts arrive subscribed, so nobody is asked to confirm again, and anyone who unsubscribed on Resend stays unsubscribed.
- Webhooks are registered at the same URLs with the same events in Postboi’s names, each with a new signing secret. See webhooks below.
Running it twice is safe: anything already on Postboi is skipped. If one thing is refused
(a domain another account owns, say), the rest still moves and the command tells you what
was left. The CLI page has the flags, including --only to move one kind at a time.
Switch the provider. Change the one line:
That’s the whole change to your app. Every send above works as it did, and you now get delivery status per message, mail.messages.get(id), a
message log and timeline in the dashboard, and postboi sync’s types, which reject a from address your account can’t send from at build time.
Two things to know first:
- Limits are per plan. The free plan sends 3,000 a month and 100 a day, the same as Resend’s free plan. Above that you need a paid plan before you switch production over. See limits.
- A new domain earns its reputation. Mailbox providers judge a sending domain partly by its history. If you send in volume, move one kind of mail first (receipts, say) and the rest a few days later, rather than everything in one go.
Want to move gradually? Switch the provider per environment with POSTBOI_PROVIDER (per-environment config), or construct the two
providers side by side and pick per send while you compare:
Or keep Resend doing the delivery. With your Resend key synced to Postboi, the dashboard’s Send via setting sends through your Resend account while Postboi keeps the log, suppressions and timeline. Your code is the same as step 2. See relay for what that does and doesn’t report back.
3. Retire Resend
Webhooks
postboi/webhooks reads Resend’s webhooks and Postboi’s into one event shape,
so you can rewrite your handler once and point either provider at it:
While Resend is your provider, it verifies the signature with RESEND_WEBHOOK_SECRET, the whsec_… from Resend’s dashboard. migrate resend registered the same URLs on Postboi with
new secrets, and bunx postboi sync writes the secret to POSTBOI_WEBHOOK_SECRET. Once
you’ve switched, the handler doesn’t change.
Resend’s email.scheduled, email.suppressed and email.received, and its contact.*, domain.* and topic.* events, aren’t read from Resend. On Postboi, a reply arrives as a received event (see replies). Postboi doesn’t send webhooks for
delayed, scheduled or suppressed mail, so migrate resend leaves those events off and says
so. A delay shows on the message’s timeline instead.
Lists and contacts
migrate resend moved your audiences. Broadcasts on Postboi are ordinary sends on your
plan, with no separate charge per contact. See lists and broadcasts.
Suppressions
Addresses Resend has suppressed for hard bounces or complaints should stay suppressed, and
Resend’s API doesn’t export them, so the command can’t move them. Add the ones you know
about before your first send from Postboi with mail.suppressions.add().
DNS
When nothing has sent through Resend for a while, remove Resend’s resend._domainkey record
and its send. subdomain records. Leave Postboi’s DKIM records and your DMARC record alone.
What carries over and what doesn’t
Moves with your code: recipients, subject, HTML and text, attachments, headers, tags,
scheduling and cancelling, idempotency keys, batches, and webhook handling. All of it is
the same mail() call on either provider.
Moves with migrate resend: domains, audiences and their contacts, and webhooks.
Stays in Resend: hosted templates and their variables, topics, inline cid: images,
API keys, past broadcasts and the suppression list.
Postboi doesn’t have yet: SDKs in languages other than TypeScript (anything else talks to the REST API), an SMTP relay for sending into Postboi, and dedicated IPs. If you depend on one of those, keep Resend as your provider and use step 1 alone. The comparison has the rest.