We Do Dev Work
We Do Dev Work
ResendSupabase AuthPostmarkSendGridLovableCursorClaude Code

Why do my app's emails land in spam (or never arrive)?

Check SPF, DKIM and DMARC for the domain your app sends from, without wiping the DNS records you already have, and stop using test SMTP in production.

The problem

For anyone whose AI-built app sends sign-up, password reset or order emails that land in spam, arrive late or never arrive. The cause is almost always the sending domain's DNS, not the code.

How to use it

  1. Download the file (or copy it).
  2. Paste it into your AI tool: Cursor, Claude Code or Lovable.
  3. Follow the checks it lists, one by one.

Context for the AI tool

You are checking why emails from this app land in spam or don't arrive, and fixing the sending setup.

  • Do not change any DNS record until you have listed every existing record for the domain, reported your findings and I have confirmed.
  • Never replace or delete an existing TXT record on the root domain. Root TXT records are shared: SPF, Google and Microsoft site verification and other services all live there. Add to them or edit the one SPF value; never overwrite the set.
  • Report findings as a table: record (name and type), current value, problem, proposed value.
  • I make the DNS changes myself unless I say otherwise. Give me the exact name, type and value.
  • Do not print API keys or SMTP passwords. Refer to them by the environment variable name.

1. Find out who sends what

What to look for: every place the app sends email from, and from which address. Typical senders: the auth provider (Supabase Auth, Clerk, Firebase), a transactional provider (Resend, Postmark, SendGrid, Amazon SES) and the domain's normal mailbox (Google Workspace, Microsoft 365).

grep -rniE "from:|sender|smtp|resend|postmark|sendgrid|nodemailer|ses" --include=*.{ts,js,tsx,svelte,py,toml,env.example} . | grep -v node_modules

How to fix: nothing yet. List each sender with its from-address and the provider behind it.

How to verify: you have a table of senders. Every from-address uses a domain you own (not a gmail.com address, not the provider's test domain).

2. Don't send production mail through test SMTP

What to look for: auth emails (sign-up confirmation, magic link, password reset) still going through the auth provider's built-in mail service. Supabase's default email service is meant for testing: it is heavily rate-limited and only delivers to members of your project team, so real users never get the email.

How to fix: configure custom SMTP in the auth provider (for Supabase: Authentication → Emails → SMTP settings) using your transactional provider's SMTP details and a from-address on your own domain.

How to verify: sign up with an address that is not on your team. The confirmation email arrives within a minute.

3. SPF: one record, with every sender in it

What to look for: the SPF record on the domain in the from-address (and on the provider's sending subdomain, if it uses one).

dig +short TXT example.com
dig +short TXT send.example.com

Problems we see: no SPF at all, two separate v=spf1 records (that makes SPF fail completely), a sender missing from the list, or so many include: entries that SPF goes over its limit of 10 DNS lookups.

How to fix: keep exactly one v=spf1 record per name, with every sender's include: in it, ending in ~all. Add or edit only that value; leave every other TXT record on the name as it is. Many providers (Resend, for example) send from a subdomain such as send.example.com with its own SPF and MX records: add those exactly as the provider shows them.

How to verify: the dig output shows one v=spf1 value per name, and all the other TXT records you listed before (site verification and so on) are still there.

4. DKIM: the provider's key is published

What to look for: the DKIM record the provider asked you to add, at its selector (for Resend resend._domainkey, for Google Workspace google._domainkey).

dig +short TXT resend._domainkey.example.com

How to fix: copy the record from the provider's domain settings exactly: name, type (TXT or CNAME) and value. Some DNS panels add the domain to the name automatically, so resend._domainkey.example.com.example.com is a common mistake.

How to verify: the provider's dashboard shows the domain as verified, and dig returns the key.

5. DMARC: start with monitoring

What to look for: a TXT record at _dmarc.example.com. Google and Yahoo require DMARC for bulk senders, and mail without it is more likely to land in spam.

dig +short TXT _dmarc.example.com

How to fix: if there is none, start with a monitoring policy that blocks nothing:

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

Read the reports for a few weeks. Move to p=quarantine only when every legitimate sender passes.

How to verify: dig returns exactly one v=DMARC1 record.

6. Test with a real inbox

What to look for: the result as a mailbox provider sees it.

How to fix: send each type of email (sign-up, reset, order) to a Gmail address and an Outlook address. In Gmail, open the message, choose Show original and read the SPF, DKIM and DMARC lines.

How to verify: all three show PASS, the messages land in the inbox, and the from-address and reply-to are on your own domain.

Did this fix it?

Email me new fixes

One short email when we publish a new fix. Nothing else, and you can unsubscribe anytime.

Still stuck? Book a call

A WDDW developer looks at your app with you. Free 15-minute call, no strings.

All free fixes

Got a dev problem?

You don't need to know exactly what's wrong. That's kind of the point.

Send the signal