Verify your sending domain
Verify a domain once and every send the app makes inherits the same authenticated identity.
This lesson is that one-time stand-up; it ships no application code.
You create a Resend account, hand Resend a sending subdomain, copy the DNS records it issues into your registrar , and wait for the domain to read Verified — the floor that gates every send in the chapter.
It gets its own lesson because DNS propagation is the slowest, flakiest step in the project: a record can take twenty minutes to resolve, so you kick it off here, move on to the code, and return when it flips green. Chapter 048 explains why each record exists; this is the doing, on your own domain.
One prerequisite: a cheap real domain.
Resend’s onboarding@resend.dev sandbox cannot prove a DKIM signature on a domain you control, which is the whole point.
Grab a .com from Namecheap, Porkbun, or Cloudflare Registrar (around $8–12 a year), or use a subdomain of one you already own.
Create the Resend account and sending keys
Section titled “Create the Resend account and sending keys”If you already signed up while reading Resend and the first verified send, sign in and skip to the keys.
-
Sign up at resend.com with the email you want to administer the account from — this is the account owner, not the sender address.
-
Open API Keys and create two keys, not one: name the first
devand the secondproduction. Set the permission on both to Sending access only, not Full access. -
Copy the
devkey (it is shown once) into your password manager. You will paste it into.envin the next lesson, not here.
Two keys, one per environment, both scoped to sending only.
A send-only key that leaks from a dev machine or CI log cannot read your domains, rotate other keys, or touch production config, and you can revoke it without disturbing the production key.
You will not use the production key until you deploy, but creating it now keeps you from reaching for the dev key in production out of convenience.
Add the sending subdomain in Resend
Section titled “Add the sending subdomain in Resend”Tell Resend which domain you send from.
-
Open Domains and click Add Domain.
-
Enter a subdomain, not your apex:
send.<your-domain>.<tld>— for examplesend.example.com, neverexample.com. -
Pick the region closest to you and confirm. Resend now shows you a table of DNS records to publish.
Sending from a subdomain gives you the isolation from the transactional subdomain split: a deliverability mistake on send., a bad campaign or a spam-complaint spike, never bleeds into the apex reputation your real mail and website depend on.
Keep the apex out of Resend.
Resend generates records scoped to that subdomain. The exact hostnames depend on what you entered; the shapes look like this:
# SPF — authorizes Resend's servers to send for the subdomainType: TXTHost: sendValue: v=spf1 include:amazonses.com ~all
# DKIM — the public key receivers use to verify the signatureType: TXTHost: resend._domainkey.sendValue: p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ... (one long string)
# MX — routes bounces and complaints back to Resend (optional but recommended)Type: MXHost: sendValue: feedback-smtp.us-east-1.amazonses.comPriority: 10The DKIM record uses the selector resend._domainkey, the prefix that tells a receiver which public key to fetch when it checks the signature.
Leave every value exactly as Resend prints it.
Publish the records at your registrar
Section titled “Publish the records at your registrar”Open the DNS panel for your domain — Advanced DNS at Namecheap, DNS Records at Porkbun, the DNS tab at Cloudflare. Add each record Resend issued, matching type, host, and value exactly.
-
Add the SPF TXT record : type
TXT, the host Resend gave you, thev=spf1 ...value pasted whole. -
Add the DKIM TXT record: type
TXT, theresend._domainkey...host, and the longp=...key as one unbroken string. -
Add the MX record if Resend listed one: type
MX, the host, the value, and the priority (usually10). -
Save. Most registrars apply DNS changes within minutes.
Almost every stalled verification traces to one of two mistakes here.
The second is the host field. Registrars disagree on whether you type the host relative to your domain or as the absolute name; getting it wrong puts the record where Resend will never find it.
Host: resend._domainkey.sendEnter only the part in front of your domain. Most registrars append the domain automatically, so the record resolves to resend._domainkey.send.yourdomain.com.
Name: resend._domainkey.send.yourdomain.comType the whole thing, ending in your domain. Cloudflare and a few others want the fully-qualified name.
Pick by what your registrar’s UI shows, then read the saved record back: either way it must resolve to resend._domainkey.send.<your-domain>.
The registrars’ DNS guides, linked at the end of this lesson, screenshot the exact field for each provider.
Verify the domain and diagnose a stall with dig
Section titled “Verify the domain and diagnose a stall with dig”Back in Resend, open your domain’s page and click Verify.
The status flips to Verified once SPF and DKIM both resolve — usually within minutes, occasionally up to 24 hours depending on your registrar’s propagation.
If it has not flipped after half an hour, stop re-clicking and diagnose with dig .
Read the record back and compare it to what Resend issued:
dig TXT resend._domainkey.send.example.com +shortA healthy response prints the full p=... key. A long key may come back as several quoted chunks on one line, which is normal; the resolver reassembles them.
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..." "...rest of the key"An empty response means the host convention is wrong, so the record is not where Resend looks: switch it to the other form from the previous step. A short or clipped value means the registrar truncated the key: re-paste it whole. Fix whichever it is, save, and click Verify again.
Publish the DMARC record
Section titled “Publish the DMARC record”Resend’s Verified badge covers SPF and DKIM.
DMARC is the third record, and you add it yourself: it tells receivers what to do when a message fails those two checks.
-
Add one more TXT record, this time at the apex, not the sending subdomain: host
_dmarc, which resolves to_dmarc.<your-domain>. -
Set the value to a monitoring-only policy:
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; -
Save.
p=none is the weakest policy on purpose: it enforces nothing and only reports.
Start here so you can see how your mail authenticates in the wild before asking receivers to quarantine or reject anything.
This chapter ships at p=none and stays there.
The rua address is where aggregate reports land.
For a side project, your own inbox is fine; production teams point it at a service that parses the raw XML into dashboards.
Set a calendar reminder.
After a week of clean reports, graduate the policy from p=none to p=quarantine, and later to p=reject — the progression that turns monitoring into enforcement instead of leaving it at none forever.
Confirm the records pass in a real inbox
Section titled “Confirm the records pass in a real inbox”The Verified badge only says your records resolve.
A test send says the receiving inbox agrees, and that agreement is the real gate.
-
On the domain’s page in Resend, use Send test email and address it to your own personal inbox (a Gmail account is ideal for the next step).
-
Open the message in Gmail, click the three-dot menu, and choose Show original.
-
Read the authentication-results panel at the top. You are looking for exactly three lines: SPF: PASS, DKIM: PASS, DMARC: PASS.
Three PASS lines is the only acceptable outcome.
If any line reads FAIL, NEUTRAL, or SOFTFAIL, the matching record is wrong or has not propagated: re-check its value with dig and re-test.
Every send in the rest of the chapter depends on this passing.
Match the seed placeholder to your domain
Section titled “Match the seed placeholder to your domain”This is the only code change in the lesson.
The seed ships a placeholder suppressed address on acme.example; point it at your verified subdomain so the suppression path you test later reads as a real address on a domain you own.
.values({ email: 'suppressed@send.acme.example', reason: 'complaint' });The starter ships the placeholder on the acme.example domain.
.values({ email: 'suppressed@send.example.com', reason: 'complaint' });Swap in your own verified subdomain.
Re-run the seed:
pnpm db:seedThis address never needs to be a real mailbox.
The suppression check fires at the application layer, inside the sendEmail wrapper you build next lesson, and returns a forbidden result before Resend is ever called; nothing is sent to the address, so matching it to your domain is purely for coherence.
Next: you paste the dev key into .env, add the email entries to the src/env.ts schema so the server fails closed without them, and build src/lib/email.ts — the send seam that reads the suppression list and requires an idempotency key before calling Resend.
External resources
Section titled “External resources”Official reference for adding a domain, reading its status, and the SPF/DKIM/DMARC records Resend issues.
Screenshots the exact Advanced DNS fields for the relative-host registrar workflow this lesson uses.
The absolute-host convention from this lesson, shown in Cloudflare's DNS panel.
The authoritative primer on DMARC tag syntax and the p=none to p=reject rollout you set a reminder for.