The day your app sends its first password reset from your own domain, 3 DNS records decide whether Gmail can authenticate it. An SPF DKIM DMARC setup is that work, in order: authorize the provider in SPF, publish the DKIM key it gives you, then add a DMARC record at p=none and read the reports. A received message’s headers prove each one passed.

What SPF, DKIM and DMARC are, and what each one proves

SPF, DKIM and DMARC are 3 DNS records that let a receiving mail server check your app’s email. SPF lists the servers allowed to send for the domain, DKIM publishes the key that verifies each message’s signature, and DMARC ties both to the visible From address and sets the policy.

They are one of several controls in how to improve email deliverability for an app. This page stays on the domain your app sends from: which records to add, where their values come from, and how to prove they pass. The table has SPF, DKIM and DMARC explained side by side: where each lives, what the receiver checks, and what a pass does and does not prove.

RecordWhere it lives in DNSWhat the receiver checksWhat a pass provesWhat it does not prove
SPFOne TXT record starting v=spf1 on the envelope-sender domainWhether the sending server is authorized for the MAIL FROM and HELO domainsThe server was allowed to use that domainAnything about the From address the reader sees; it can fail when a forwarder keeps the original sender address
DKIMA key at <selector>._domainkey.<domain>, as TXT or as a CNAME the provider printsThe signature in the DKIM-Signature header against the published public keyThe signing domain (d=) took responsibility, and the content was not changed in transitThat the signing domain is yours: a message “can bear a valid signature from any domain”
DMARCOne TXT record starting v=DMARC1 at _dmarc.<domain>Whether SPF or DKIM passed for a domain aligned with the From domainThe visible From domain was used with the owner’s authorizationThat the mail is safe or wanted

The difference between SPF, DKIM and DMARC fits in one line each: SPF is about the server, DKIM is about the message, and DMARC is about the visible sender and what to do when the check fails. In cyber security terms, SPF is an anti-spoofing control; Google describes it as a way to stop “spammers from sending unauthorized messages that appear to be from your domain”. It does not encrypt anything: Google lists a TLS connection as a separate requirement.

Gmail and Yahoo turned these records into requirements in February 2024, in two tiers. Google’s email sender guidelines require SPF or DKIM from everyone who sends to Gmail accounts, and SPF, DKIM and DMARC from senders of more than 5,000 messages a day, with a DMARC policy that “can be set to none”. Yahoo’s sender best practices set the same split: SPF or DKIM for all senders, both plus a DMARC policy of at least p=none for bulk senders. Google’s page does not exempt transactional mail, and it recommends that you “always set up SPF, DKIM, and DMARC for your domains”.

Setting up and proving these three records is deliverable 11.1 of the Production Hardening Sprint, which verifies and configures SPF, DKIM, and DMARC for the transactional sending domain.

What an SPF record is, and what the check runs against

An SPF record is one TXT record, starting v=spf1, that lists the servers allowed to send mail for a domain. The check runs against the envelope sender, shown as Return-Path, and the HELO name, not the visible From address, and it fails with a permanent error past 10 DNS lookups.

send.example.com.  TXT  "v=spf1 include:<host-your-provider-prints> ~all"
; v=spf1 opens the record; include: authorizes the provider's servers; ~all softfails everything else

The mechanisms in the middle name who may send: include: evaluates another domain’s record, ip4: matches an address range, and a and mx match the addresses behind a domain’s A and MX records. The ending decides what happens to everyone else: -all is “an explicit statement that the client is not authorized”, while ~all is “a weak statement” that the host “is probably not authorized”. Never copy the include: value from a guide: your provider’s dashboard prints it, as the setup section shows.

For an app, the detail that matters is the identity being checked: SPF authentication checks are performed against the envelope sender, the address given in the SMTP MAIL FROM command, and against the HELO name, not against the From line the reader sees. On a received message that envelope sender appears as Return-Path, because RFC 5321 has the delivering server insert “a return-path line” that keeps the address from the MAIL command. RFC 7208 even calls checking other identities “NOT RECOMMENDED”.

The envelope sender is the provider’s own bounce domain unless the provider’s records move it onto yours: Resend puts it on a send subdomain by default, Postmark does it once you add its custom Return-Path record, and Amazon SES uses a subdomain of amazonses.com until you set up a custom MAIL FROM domain. So SPF can pass for the provider’s domain and still count for nothing toward DMARC on yours.

Two rules catch people. A name may hold only one SPF record: two records produce “permerror”. And the terms that trigger DNS lookups (include, a, mx, ptr, exists and redirect) are capped at 10 per evaluation, past which the result is also “permerror”; ip4, ip6 and all cost nothing. So a good SPF record for an app is a single record on the name, with your provider’s include:, a ~all or -all ending, and no more than 10 lookups.

Alignment: why DMARC can fail when SPF and DKIM both pass

DMARC alignment means the domain that passed SPF or DKIM matches the domain in the From address. One aligned pass is enough. A message whose signature and return path both use the provider’s shared domain passes SPF and DKIM and still fails DMARC, because neither domain is yours.

RFC 9989 gives two modes. Relaxed alignment accepts any domain with “the same Organizational Domain”, so bounce.example.com aligns with example.com; strict alignment needs the two to be “identical”. The aspf and adkim tags pick the mode for SPF and DKIM, both default to relaxed, and the RFC notes that “nearly all Domain Owners have found relaxed alignment sufficient”. For SPF, DMARC looks only at the MAIL FROM domain, never the HELO name.

What the header shows (From: app@example.com)Aligned?Counts toward DMARC?
dkim=pass header.d=example.comYes, identicalYes: DMARC passes on this alone
dkim=pass header.d=<provider's shared domain>NoNo: DKIM passed, but for someone else’s domain
spf=pass smtp.mailfrom=<provider's bounce domain>NoNo: SPF passed for the provider
spf=pass smtp.mailfrom=bounce.example.comYes, under relaxed mode (same organizational domain)Yes, with aspf left at its default

For an app, I get DKIM aligned first, then add the custom return path if the provider offers one. DKIM goes first because relays “typically make no substantive change to the message content and thus preserve the DKIM signature”, while SPF can fail when a forwarder resends mail with the original sender address.

What goes wrong without it

Each row below is something you can see from the outside, in a mailbox or a provider log.

What you seeThe causeThe record that fixes it
The password reset lands in spam, or Gmail shows “via” and a website name next to the senderGmail shows “via” when “the domain it was sent from doesn’t match the domain in the ‘From:’ address”: the mail went out signed for the provider’s domainA DKIM record for your domain; Gmail’s own fix is to sign with “a DKIM signature that is associated with your domain”
Gmail rejects the message with a 5.7.26 errorNo SPF or DKIM for the domain, and the receiver now requires oneThe full setup on this page
SPF reports permerrorTwo SPF records on one name (one from the mailbox host, one from the app’s provider) or more than 10 lookupsOne merged record
Anyone can send mail that claims your domain, and nothing tells youNo DMARC record, so no receiver sends you reportsDMARC at p=none with a rua address
Everything passes and mail still lands in spamA DMARC pass “does not guarantee that delivery to the recipient’s inbox would be safe or desirable”None of the three records

At p=none the record collects reports and states no preference about failing mail. Blocking forgeries is a later job, part of how to stop email spoofing. The last row is a content and reputation question, the subject of transactional emails landing in spam.

If the app still sends auth mail through Supabase’s built-in mailer, nothing on your domain is in play yet; the Supabase email rate limit and the custom SMTP fix is where that move starts.

SPF DKIM DMARC setup for an app that sends through a provider: the order and the records

An SPF, DKIM and DMARC setup for app email is 6 steps: pick the sending domain, add it in the provider’s dashboard, publish the SPF or return-path record, publish the DKIM record, send a real message once the provider shows the domain verified, then add DMARC at p=none with a reporting address.

  1. 01 Pick the sending domain: a subdomain such as mail.example.com keeps app mail apart from your own mailbox.
  2. 02 Add that domain in the provider's dashboard and copy every record it prints, exactly as printed.
  3. 03 In your DNS panel, add the SPF or return-path record, in whatever type the provider printed: TXT, MX or CNAME.
  4. 04 Add the DKIM record at the selector name the provider gave you.
  5. 05 Wait until the provider shows the domain as verified, then trigger a real message from the app.
  6. 06 Add a TXT record at _dmarc with v=DMARC1; p=none and a rua address for reports.

None of the six steps needs a terminal. Each is a dashboard click path or a DNS-panel form, named per provider in the table below, and the command line only appears in the checks further down. Choosing the provider and tracking what it delivers belong to transactional email best practices; this page starts once a provider is chosen.

Pick the sending domain: a subdomain for app mail

Resend’s docs “strongly recommend sending emails from a subdomain (e.g., notifications.example.com) instead of your root domain”, and its overview gives the reason: to “isolate your sending reputation”. A subdomain such as mail. or notify. also keeps the app’s SPF away from the record your own mailbox already uses on the root, so the one-record rule stays easy to keep.

Under relaxed alignment, a DKIM signature for mail.example.com also counts as aligned for a From address on example.com, because both share an organizational domain. The simplest setup still sends From the same domain you verified with the provider.

If the root already has an SPF record for Google Workspace or Microsoft 365, and a provider asks you to add SPF on that same name, add its include: to the existing record. A second record on the same name breaks SPF there, as the SPF section above explains. Who holds the registrar login, and what else sits in the zone, is part of adding HTTPS to a website and the DNS hygiene around it.

Where the records come from: Resend, Postmark, SendGrid and Amazon SES

Your provider generates the record values. Copy them from its dashboard; Resend’s docs put it plainly: “Copy and paste the records to avoid configuration errors.” The table shows the shape of what each one prints, from its own docs.

ProviderWhere in the dashboardRecords it printsCustom return pathDocs checked
ResendDomains page, Add Domain; values on the domain’s Records tabA DKIM TXT at resend._domainkey on the domain you added, and SPF on its send subdomain: TXT plus MX on older domains; domains created after August 2026 may show CNAME records insteadOn by default: Return-Path defaults to send.<your domain>; a custom one is set only when the domain is added2026-10-04
Amazon SESSES console, Configuration, Identities, Create identity; values under Publish DNS recordsThree CNAME records for Easy DKIM (2048-bit by default)Custom MAIL FROM domain: an MX record and an SPF TXT record on a subdomain you own2026-10-04
PostmarkSender Signatures page, DNS SettingsOne DKIM record as TXTA CNAME added as a separate step; once verified, mail “will start to pass SPF alignment”2026-10-04
SendGridSettings, Sender Authentication, Domain Authentication, Get StartedAutomated security on (the default): CNAMEs for SPF and two DKIM selectors, plus a DMARC TXT. Off: an MX record and three TXT records”Use custom return path” under Advanced Settings2026-10-04

Docs checked on 2026-10-04: Resend’s domain docs, Postmark’s domain verification guide, SendGrid’s domain authentication guide, Amazon SES Easy DKIM and Amazon SES custom MAIL FROM domain.

A new Amazon SES account starts in the sandbox, where in AWS’s words “You can only send mail to verified email addresses and domains, or to the Amazon SES mailbox simulator”, with “a maximum of 200 messages per 24-hour period”, and the status is “unique per each AWS Region”. Until production access is granted, the test mailboxes in the checks below have to be verified identities.

Auth mail follows the same logic. Supabase Auth sends through whatever custom SMTP service you configure, so the records are that provider’s. Clerk and Firebase Authentication send the mail themselves and show their own records for your domain: Clerk says it “uses DNS records to provide session management and emails verified from your domain”, and Firebase says to “Verify your domain by adding DNS records in your domain registrar”.

Add the DKIM record to DNS

A DKIM record is a public key published at selector._domainkey under the sending domain, as a TXT record or a CNAME to the provider’s key. Keys of 2048 bits are the current guidance, and the selector lets each sending tool sign with its own key.

  1. Copy the selector (the host name) and the value from the provider’s records page.
  2. In the DNS panel, create the record at <selector>._domainkey, plus the subdomain if the provider uses one. If your panel adds the domain itself, pasting the full name doubles it; AWS warns that “Some providers append the domain name without indicating that they’ve done so.”
  3. Choose the type the provider printed. A CNAME points at a key the provider hosts, so the provider can change the key without you; SendGrid says it “can create and update your SPF and DKIM records on your behalf”. A TXT holds the key itself.
  4. On Cloudflare, set a CNAME to DNS only. Cloudflare’s proxy status docs say records used for purposes other than web traffic “should not be proxied”, and a TXT record is always DNS only.
  5. Paste the TXT value whole. If the panel shows a long key as several quoted strings, receivers join them: RFC 6376 says they “MUST be concatenated together before use with no intervening whitespace”. Resend lists “Truncating long values” and “Adding extra quotes or spaces” among the common DKIM mistakes.
s1._domainkey.mail.example.com.  TXT  "v=DKIM1; k=rsa; p=<public-key-your-provider-prints>"

The receiver finds this record by joining the s= selector and the d= domain from the signature, as in RFC 6376’s own example, where they produce “foo.bar._domainkey.example.com”. RFC 8301 sets the floor: signers “MUST use RSA keys of at least 1024 bits” and “SHOULD use RSA keys of at least 2048 bits”. Two tools can sign for one domain because each gets its own selector; SendGrid’s setup has a “Use a custom DKIM selector” option for when another service already uses the same one.

How to set up DMARC for a domain, starting at p=none

A DMARC record is one TXT record at _dmarc under the domain. It carries one of three policies, none, quarantine or reject, plus the address for aggregate reports, which receivers are asked to send at least daily. I start at p=none after a real message passes, so a half-finished setup never blocks your own mail.

A minimal DMARC record example at that starting point, with a reporting address:

_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"

Tag by tag, from RFC 9989:

  • v=DMARC1 must come first; a record that starts any other way is ignored.
  • p is the policy for the domain and, unless overridden, its subdomains.
  • rua is where aggregate reports go; leave it out and receivers “MUST NOT generate aggregate feedback reports”.
  • sp sets a separate policy for existing subdomains, np one for subdomains that do not exist.
  • adkim and aspf choose relaxed (r, the default) or strict (s) alignment.

RFC 9989 (May 2026) obsoletes RFC 7489, lists pct, ri and rf as “historic” and adds a t tag for testing a policy, so a guide that still sets pct= was written against the older text.

p= valueWhat the domain owner tells receivers (RFC 9989)
nonethe owner “offers no expression of preference”; reports still go to rua
quarantinethe owner “considers such mail to be suspicious”
rejectfailures are “a clear indication that the use of the domain name is not valid”

Receivers stay in charge: failing mail is handled by “the Mail Receiver’s local policies”, which “may take into account” yours. Google Workspace’s DMARC setup page sets a longer wait before the record goes in: “DKIM and SPF should be authenticating messages for at least 48 hours before turning on DMARC”.

The reports are XML aggregate reports defined in RFC 9990, and receivers “SHOULD send aggregate reports on at least a daily basis”. For a small sender, my suggestion costs nothing: a dedicated mailbox on the same domain and a look about once a week at who is sending as you. Keep that mailbox on your own domain, because “reporting addresses generally have to be inside the domains about which reports are requested”; an address elsewhere receives them only if that domain publishes the authorization record RFC 9990 describes under “Verifying External Destinations”. Report-reading services exist for when the XML gets too much.

A subdomain falls under the root’s policy unless it publishes its own record or the root sets sp. Climbing from p=none to quarantine and reject, and deciding when, belongs with how to stop email spoofing.

How to verify it

Sending-domain authentication is verified with 5 checks: one SPF record on the return-path domain, the DKIM key at its selector, one DMARC record, and a message your app really sent showing spf=pass, dkim=pass and dmarc=pass in two different mailboxes. Keep the lookups and the header lines as evidence.

The procedure below comes from the RFCs and the mailbox providers’ help pages, and each check can fail on a domain that is not set up.

  1. 01 SPF: look up TXT on the envelope-sender domain, the return-path or MAIL FROM name your provider printed (the smtp.mailfrom= domain in a received header names it). Pass: exactly one v=spf1 record, directly or through the provider's CNAME. Fail: none, or two on the same name. The From domain itself may correctly have none.
  2. 02 DKIM: look up <selector>._domainkey.<domain>. Pass: the key comes back, directly as TXT or through a CNAME. Fail: NXDOMAIN, or the record sitting at a doubled name such as s1._domainkey.mail.example.com.mail.example.com.
  3. 03 DMARC: look up TXT at _dmarc.<domain>. Pass: one record that starts with v=DMARC1. Fail: no record, or one that starts with anything else, which receivers ignore.
  4. 04 Gmail: trigger a real message from the app, such as a password reset, to a Gmail mailbox and open its full header. Pass: spf=pass, dkim=pass with your domain in header.d= or header.i=, and dmarc=pass. Fail: any of the three missing or not pass, or dkim=pass only for the provider's domain.
  5. 05 Second receiver: the same kind of message in an Outlook.com or Yahoo mailbox shows the same three passes. Fail: any result that differs from Gmail's.

Evidence to keep: the three lookup outputs with the date, and the Authentication-Results lines from both mailboxes, pasted as text rather than screenshots.

In the sprint, we verify deliverable 11.1 by inspecting DNS and received message headers for the configured authentication results.

Inbox placement, seed tests and scoring tools such as mail-tester sit outside these checks; they belong to how to test email deliverability.

DKIM key checker: what a lookup can and cannot validate

A DKIM key checker is a DNS lookup: it confirms the public key exists at the selector, parses, and is at least 1024 bits. It cannot test the private key, which the provider holds. Only a received message that says dkim=pass proves the pair matches.

Start in the browser: Google Admin Toolbox Dig takes the record name, and you pick TXT or CNAME. In a terminal, the same lookup is one line with either tool:

dig s1._domainkey.mail.example.com TXT +short
nslookup -type=TXT s1._domainkey.mail.example.com

A DKIM public key check reads DNS only: the record exists, the p= value parses, and the key meets the RFC 8301 sizes quoted in the DKIM section. A DKIM private key check is not a lookup at all, since the private key stays with whoever signs your mail; Microsoft says of its own keys that “The private keys that are used to sign the message are inaccessible.” The one way to validate the DKIM key pair end to end is a message that arrives with dkim=pass.

What you want to checkHow
The record exists at the selectorA TXT or CNAME lookup of <selector>._domainkey.<domain>
The key is long enoughRead the key length from a checker’s output: at least 1024 bits, 2048 preferred
The private key matches the public keyA received message showing dkim=pass for your domain
The selector, when you do not know itThe s= value in the DKIM-Signature header of any message the app sent

Online checkers from MXToolbox, EasyDMARC and dmarcian run this same kind of lookup: MXToolbox says its tool “tests the ability to retrieve the DKIM public key using a domain and a selector”, and EasyDMARC says its check “can prove that a public record is associated with a given selector”.

Check email headers for authentication results

Authentication results are in the Authentication-Results header that the receiving server adds when it checks a message. It carries a result for spf, dkim and dmarc, each with the domain it was checked against, and comparing those domains with the From address shows whether the pass is aligned.

In Gmail, open the message and, next to Reply, click More, then Show original; “In a new window, the full header shows.” In Outlook.com or the new Outlook, select More actions, then View, then View message details; in classic Outlook, open the message and click File, then Properties, and read the Internet headers box. The steps are in Gmail’s guide to viewing headers and Outlook’s guide to message headers.

Read the Authentication-Results lines. RFC 8601 defines the header, and says it “is added at the top of the message as it transits MTAs that do authentication checks”. The block below is a constructed illustration, built from RFC 8601’s field names and RFC 9989’s dmarc entries with example.com in place of a real domain; it is not copied from a real message.

Authentication-Results: mx.receiver.example;
  spf=pass smtp.mailfrom=bounce.example.com;
  dkim=pass header.i=@example.com header.s=s1 header.d=example.com;
  dmarc=pass policy.dmarc=none header.from=example.com
Return-Path: <bounces@bounce.example.com>
From: Example App <no-reply@example.com>

smtp.mailfrom is the envelope sender SPF checked, header.d the domain that signed, header.s the selector, and header.from the From domain DMARC compared them with. Two comparisons tell you about alignment: smtp.mailfrom against header.from, and header.d against header.from. RFC 8601’s own examples show the DKIM domain as header.d= in one place and as header.i= in another, so look for your domain in either. Gmail’s raw header text, copied with its Copy to clipboard button, is the evidence to keep.

Verify the DMARC policy with a test message

Trigger the real thing: a password reset or a signup confirmation from production, not a test send from the provider’s dashboard, which proves the dashboard’s settings rather than the ones your code uses. Send it to mailboxes at two different receivers, open each header and read dmarc=pass and the policy the receiver recorded.

Then wait for the reports. They come on each receiver’s own schedule (the RFC 9989 line on reports above), so my advice is to give it a day or so before you look in the rua mailbox. The report should list your provider’s sending sources as passing and nothing else. A failing source that turns out to be yours, such as a second tool sending as the domain, gets its own DKIM selector before the policy is ever tightened.

The message under test is usually the verification email, and its wording is a separate subject: a verification email template.

Where the sprint does this

Area 11 of the published scope, Email & communications, contains 3 of the 123 deliverables : 11.1 Sending-domain authentication, 11.2 Transactional delivery tracking and 11.3 Bounce and complaint handling, the subject of an email bounce handling checklist. Their results go into deliverable 13.1, the Production readiness report, which accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. Hosting, paid tools, and API usage remain in your accounts. The verify line for each one is in all 123 deliverables in the published scope.

Common questions about email authentication records

Is DMARC mandatory now?

For bulk senders, yes: since February 2024 Google requires SPF, DKIM and DMARC from anyone sending more than 5,000 messages a day to Gmail accounts, with the policy allowed at none, and Yahoo requires a DMARC policy of at least p=none from bulk senders. Below the bulk tier both ask for SPF or DKIM, and no RFC forces DMARC on anyone: RFC 9989 says a domain owner can opt out “simply by not publishing a DMARC Policy Record”.

Do you need SPF if you use DKIM?

Not for DMARC: one aligned DKIM pass is enough for a message to pass. Google and Yahoo still require both SPF and DKIM from bulk senders, and RFC 9989 recommends using both, so set up both.

How long does DKIM take to verify?

Anywhere from minutes to a few days, depending on the provider’s check and on DNS. Resend says a domain will “often verify within 15 minutes” but DNS “can occasionally take up to 72 hours”; Postmark says DKIM shows verified “within 48 hours”; SendGrid says DNS changes “can take up to 48 hours to apply”; and Amazon SES allows “up to 72 hours”.

What happens if you have no SPF record?

SPF returns “none”, not “fail”: RFC 7208 uses that result when “no SPF records were retrieved from the DNS”. What happens next is the receiver’s call. Gmail requires SPF or DKIM from every sender, and mail with neither “might be marked as spam or rejected with a 5.7.26 error”.

How do I add Microsoft DKIM to my domain?

In the Microsoft Defender portal, open Email authentication settings, select the DKIM tab, try to enable your domain, copy the two CNAME values for selector1._domainkey and selector2._domainkey, create both at your registrar, then turn on “Sign messages for this domain with DKIM signatures”. These selectors sign your Microsoft 365 mailbox mail and sit next to your app provider’s selector; they do not replace it.