So DNS finally turns green in Resend (or SendGrid, Postmark, SES). The client’s real domain is verified. We merge the dashboard change and then signup still throws “Error sending confirmation email” because one API route still sends from noreply@old-agency-domain.com.

That failure is predictable. Transactional email rarely lives in one file. It hides in magic-link helpers, cron digests, Supabase hooks, Java Mailer classes, and .env.example values nobody opened.

This post is how we inventory every outbound From, centralize it behind one config surface, and verify we are not still mailing from a domain the provider will reject.

References we keep open:

  • Resend domains: Managing Domains.
  • Resend send API: Send Email.
  • General alignment (any provider): SPF, DKIM, and DMARC on the same domain as the From address.

What we will learn

  • grep-first audit that catches hardcoded addresses, not only the main mailer module.
  • How to centralize From (and optional Reply-To) so the verified domain changes once.
  • What to check when the app spans multiple repos or runtimes (Next.js API routes + admin + auth).
  • staging send checklist before we tell the client email is fixed.

Prerequisites

  • The production domain (e.g. clientjobs.ca) shows verified in the email provider dashboard.
  • We have access to the repo(s), deployment env vars, and ideally a non-prod API key.
  • We know whether auth emails (Supabase, Firebase, Cognito) are separate from app emails. They often are.

Why “we updated Resend” is not enough

Providers validate the From domain (and sometimes the exact mailbox) at send time. Common failure modes:

SymptomLikely cause
Toast / 500 on signupMagic-link or confirmation still uses old @ domain
Some emails deliver, some bounceOnly one code path was updated
Dev works, prod failsRESEND_FROM set in staging, missing in production
Marketing receives, transactional does notTwo different integrations; we fixed one

Rule of thumb: treat From address like a secret we rotate by config, not like a string we sprinkle in call sites.


Step 1: Inventory before we edit (grep playbook)

Run from repo root (adjust paths). We are hunting addressesprovider SDKs, and env keys.

Email addresses

# Obvious From patterns
rg -n "from:\s*['\"]|from:\s*`|\"from\"|From:" --glob "*.{ts,tsx,js,jsx,java,py,go,env*}"
# Any @ in mail-related files (noisy but catches stragglers)
rg -n "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}" src/ --glob "!**/node_modules/**"
# noreply / onboarding / old vendor domains
rg -n "noreply@|no-reply@|onboarding@|mail@|@resend\.dev|@sendgrid" .

Provider entry points

rg -n "resend|Resend|sendgrid|SendGrid|postmark|nodemailer|@aws-sdk/client-ses|JavaMail|smtp" \
--glob "*.{ts,tsx,js,java,properties,yml,yaml}"

Config and templates

rg -n "RESEND|MAIL_FROM|SMTP_FROM|SENDGRID|EMAIL_FROM" .
rg -n "mailto:" public/ templates/ supabase/

Opinion: export hits to a spreadsheet: filelinecurrent Fromemail type (signup, receipt, scraper alert, etc.), owner.


Step 2: Map the real send graph

Most codebases look like one of these:

We want zero paths from feature code straight to D with a hardcoded domain.

Questions we answer on the map:

  1. How many emails.send (or equivalent) call sites exist?
  2. Is there already a lib/send-email/ or Mailer class?
  3. Do Supabase Auth / NextAuth emails bypass our Resend wrapper?
  4. Are Reply-To addresses still on Gmail personal inboxes?

Step 3: Centralize the verified domain

Pattern we have shipped on Next.js + Resend:

Before: each caller sets from: 'Brand <bipoc@oldvendor.com>'.

After: one exported constant + env override.

// lib/send-email/resend.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_KEY);
/** Verified domain only. Override in env for staging aliases. */
export const resendFrom =
process.env.RESEND_FROM ?? 'Brand Name <noreply@clientjobs.ca>';
// lib/send-email/send-magic-link.ts
import resend, { resendFrom } from './resend';
await resend.emails.send({
from: resendFrom,
to: email,
subject: 'Sign in',
html: '...',
});

Wire RESEND_FROM through next.config / deployment env the same way as RESEND_KEY, or staging will drift.

Display name matters

Resend accepts Name <addr@verified.domain>. We pick one brand string and reuse it so inboxes look consistent.

Reply-To is separate

Support may want hello@clientjobs.ca while From stays noreply@. That is fine if hello@ is a real mailbox or alias. Document both in env:

RESEND_FROM="Brand Jobs <noreply@clientjobs.ca>"
RESEND_REPLY_TO=hello@clientjobs.ca

Step 4: Multi-repo and legacy stacks

Real client work rarely sits in one folder.

SurfaceWhere From hides
Public site (Next.js)API routes, server actions, auth callbacks
Admin appseparate repo, duplicate resend.ts if we are not careful
Java / PHP monolithMailer.javaapplication.properties, Freemarker footers
Serverless cronsmall Lambda with its own env block
Auth providerSupabase dashboard SMTP / custom SMTP, not our TypeScript at all

Workflow: repeat the grep playbook per repo. Prefer one shared npm package or copied resendFrom snippet with a comment linking to the canonical env names.

If Supabase sends auth mail, either:

  • configure Custom SMTP in the Supabase project to Resend’s SMTP credentials, or
  • disable built-in mail and send via our API after signup.

Skipping that split is how “we fixed the codebase” but login email still fails.


Step 5: Replace, do not whack-a-mole

Order that keeps us sane:

  1. Add resendFrom (or mailFrom) export without changing behavior.
  2. Replace call sites one module at a time (magic link, employer invite, job alert, …).
  3. Delete the last hardcoded @old-domain string; grep again until clean.
  4. Update .env.example, README, and deployment docs in the same PR.
  5. Set production RESEND_FROM before merge if the default in code is already the new domain.

Avoid leaving a “temporary” old address in a comment. Someone will paste it back.


Step 6: Verify sends (staging checklist)

CheckHow
From domain matches verified zoneRead full message headers in Resend dashboard or mail client
Signup / magic linkTrigger once in staging; confirm delivery
Each email type we listed in the inventoryOne test per row in the spreadsheet
Env parityProd has RESEND_KEY and RESEND_FROM if we rely on override
DNSSPF + DKIM show pass; DMARC not failing on the new From

Resend’s dashboard logs the accepted from field. If it differs from what we thought we shipped, we still have a stray call site.

For local dev, we might route to smtp4dev or Resend’s test key. The verified-domain rule still applies if we hit the real API.


Pitfalls we plan for

  • Only updating the dashboard, not code. Verification enables the domain; code must use it.
  • Using @resend.dev in production after prototyping. Replace before launch.
  • Mixed apex and www in links vs From domain. Marketing cares; DMARC might care too.
  • Forgotten cron or scraper alerts low traffic, old From, discovered months later.
  • Secrets in git history old API keys are a different ticket, but grep sometimes surfaces them while hunting From.

What we tell the client / manager

We audited all transactional send paths, pointed them at noreply@yourdomain.com on the verified domain, and documented RESEND_FROM for future changes. Auth email [is / is not] on the same provider; here is the checklist for the remaining path.

Short, provable, tied to the grep inventory.


Closing

Switching to a verified domain is two jobs: DNS in the provider, and archaeology in the repo. The second job wins or loses the signup flow.

We do not need to memorize every file. We need a repeatable search, one config knob for From, and a test row per email type. After that, the next domain migration is boring, which is the point.

Leave a Reply

Discover more from Ayush Raj

Subscribe now to keep reading and get access to the full archive.

Continue reading