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
- A grep-first audit that catches hardcoded addresses, not only the main mailer module.
- How to centralize
From(and optionalReply-To) so the verified domain changes once. - What to check when the app spans multiple repos or runtimes (Next.js API routes + admin + auth).
- A 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:
| Symptom | Likely cause |
|---|---|
| Toast / 500 on signup | Magic-link or confirmation still uses old @ domain |
| Some emails deliver, some bounce | Only one code path was updated |
| Dev works, prod fails | RESEND_FROM set in staging, missing in production |
| Marketing receives, transactional does not | Two 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 addresses, provider SDKs, and env keys.
Email addresses
# Obvious From patternsrg -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 domainsrg -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: file, line, current From, email 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:
- How many
emails.send(or equivalent) call sites exist? - Is there already a
lib/send-email/orMailerclass? - Do Supabase Auth / NextAuth emails bypass our Resend wrapper?
- 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.tsimport { 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.tsimport 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.
| Surface | Where From hides |
|---|---|
| Public site (Next.js) | API routes, server actions, auth callbacks |
| Admin app | separate repo, duplicate resend.ts if we are not careful |
| Java / PHP monolith | Mailer.java, application.properties, Freemarker footers |
| Serverless cron | small Lambda with its own env block |
| Auth provider | Supabase 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:
- Add
resendFrom(ormailFrom) export without changing behavior. - Replace call sites one module at a time (magic link, employer invite, job alert, …).
- Delete the last hardcoded
@old-domainstring; grep again until clean. - Update
.env.example, README, and deployment docs in the same PR. - Set production
RESEND_FROMbefore 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)
| Check | How |
|---|---|
| From domain matches verified zone | Read full message headers in Resend dashboard or mail client |
| Signup / magic link | Trigger once in staging; confirm delivery |
| Each email type we listed in the inventory | One test per row in the spreadsheet |
| Env parity | Prod has RESEND_KEY and RESEND_FROM if we rely on override |
| DNS | SPF + 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.devin 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_FROMfor 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