Skip to content
Chapter 50Lesson 2

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.

Registrar DNS SPF / DKIM / DMARC TXT records
Resend sends from send.<your-domain>
Receiving inbox validates the signature → PASS / FAIL
The three records you publish at your registrar are what let the receiving inbox confirm a message really came from you. Shape only — the alignment mechanics are owned by chapter 48.

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.

  1. Sign up at resend.com with the email you want to administer the account from — this is the account owner, not the sender address.

  2. Open API Keys and create two keys, not one: name the first dev and the second production. Set the permission on both to Sending access only, not Full access.

  3. Copy the dev key (it is shown once) into your password manager. You will paste it into .env in 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.

Tell Resend which domain you send from.

  1. Open Domains and click Add Domain.

  2. Enter a subdomain, not your apex: send.<your-domain>.<tld> — for example send.example.com, never example.com.

  3. 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 subdomain
Type: TXT
Host: send
Value: v=spf1 include:amazonses.com ~all
# DKIM — the public key receivers use to verify the signature
Type: TXT
Host: resend._domainkey.send
Value: p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ... (one long string)
# MX — routes bounces and complaints back to Resend (optional but recommended)
Type: MX
Host: send
Value: feedback-smtp.us-east-1.amazonses.com
Priority: 10

The 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.

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.

  1. Add the SPF TXT record : type TXT, the host Resend gave you, the v=spf1 ... value pasted whole.

  2. Add the DKIM TXT record: type TXT, the resend._domainkey... host, and the long p=... key as one unbroken string.

  3. Add the MX record if Resend listed one: type MX, the host, the value, and the priority (usually 10).

  4. 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.send

Enter only the part in front of your domain. Most registrars append the domain automatically, so the record resolves to resend._domainkey.send.yourdomain.com.

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:

Terminal window
dig TXT resend._domainkey.send.example.com +short

A 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.

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.

  1. Add one more TXT record, this time at the apex, not the sending subdomain: host _dmarc, which resolves to _dmarc.<your-domain>.

  2. Set the value to a monitoring-only policy:

    v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com;
  3. 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.

The Verified badge only says your records resolve. A test send says the receiving inbox agrees, and that agreement is the real gate.

  1. 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).

  2. Open the message in Gmail, click the three-dot menu, and choose Show original.

  3. 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.

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.

scripts/seed.ts
.values({ email: 'suppressed@send.acme.example', reason: 'complaint' });

The starter ships the placeholder on the acme.example domain.

Re-run the seed:

Terminal window
pnpm db:seed

This 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.