Email Deliverability — outbound auth audit
Why this exists
On 2026-05-26, Anthony (founder's nephew, IT professional) signed up for a free trial and reported that every email landed in junk. The two contributors we already addressed in this PR:
- Plain-text alternative bodies are now sent with every HTML message (
htmlToPlainTextinemail-provider.ts). Major filters score multipart/alt noticeably higher than HTML-only. - The
/signinflow at/api/onboard/resend-linknow has a public UI, so we'll see deliverability symptoms instead of silently failing recoveries.
The remaining contributor is outbound authentication — SPF, DKIM, and DMARC. These records live in the registrar (Porkbun) and the Resend dashboard, not the codebase, so they need a founder hand to verify.
This runbook is what you run to close the loop.
What gets sent and from where
| Stream | Subject types | Volume | From | Provider |
|---|---|---|---|---|
| Transactional (auth, billing, ops) | Welcome / magic link / receipt / lifecycle / inbox notifications | low (1–50/day) | ChurchWiseAI <hello@churchwiseai.com> | Resend primary → AWS SES fallback |
| Outreach (cold + warm marketing) | Cold prospects, win-back, newsletter | high (~hundreds/day) | varies, currently hello@churchwiseai.com | Resend |
| Founder correspondence | One-off replies | low | john@churchwiseai.com | Google Workspace (out of scope) |
The deliverability problem: today, all three streams share the same SPF/DKIM identity. A bad cold-outreach run can tank the reputation of the magic-link stream. The hardening plan below separates them.
Action 1 — verify what's published today
Run these from any terminal (Windows PowerShell or Mac/Linux). Both dig and nslookup are fine.
# SPF
nslookup -type=TXT churchwiseai.com
# DMARC
nslookup -type=TXT _dmarc.churchwiseai.com
# Resend DKIM (selector is "resend")
nslookup -type=TXT resend._domainkey.churchwiseai.com
# SES DKIM — three selectors, all configured at once when SES verifies the domain
nslookup -type=TXT <SELECTOR>._domainkey.churchwiseai.com
You can also paste churchwiseai.com into mxtoolbox.com/SuperTool.aspx and run SPF Record Lookup, DMARC Lookup, and DKIM Lookup — it'll show what's published with friendly explanations.
What you want to see
SPF (TXT record on root churchwiseai.com):
v=spf1 include:_spf.google.com include:amazonses.com include:_spf.resend.com -all
include:_spf.google.com— Google Workspace (founder mail)include:amazonses.com— SES fallbackinclude:_spf.resend.com— Resend (primary)-all— reject mail from anything not listed (strict, what we want)
If you see ~all instead of -all, it's "soft fail" — still acceptable but spam filters trust it less. Tighten to -all once Resend, SES, and Google are all known-good.
DMARC (TXT record on _dmarc.churchwiseai.com):
v=DMARC1; p=none; rua=mailto:dmarc@churchwiseai.com; ruf=mailto:dmarc@churchwiseai.com; fo=1; adkim=r; aspf=r
- Start with
p=none(monitoring only). Resend and SES dashboards will start reporting alignment after a few days. - After a week of clean reports, move to
p=quarantine(suspicious mail → recipient's junk). - After another week of clean reports, move to
p=reject(suspicious mail → discarded by recipient). rua=andruf=should point to an inbox you actually read —dmarc@churchwiseai.com(set up forwarding to john@) is fine.
DKIM (Resend):
- Resend dashboard → Domains →
churchwiseai.com→ "Add records." Resend gives you exact TXT records to paste into Porkbun. Selector is typicallyresend._domainkey.churchwiseai.com. - After paste, click Verify in Resend. Must show green ✅ before relying on it.
DKIM (SES):
- SES console → Verified identities →
churchwiseai.com→ "Authentication." SES will give three TXT records (selector1/2/3._domainkey.churchwiseai.com). All three must be present in Porkbun.
Action 2 — split the subdomains (recommended)
The current single-identity setup means a cold-outreach reputation hit affects magic-link delivery. Industry standard is to split:
| Use | New from-address | DNS root |
|---|---|---|
| Transactional (welcome, magic link, receipt, lifecycle, inbox notifications) | ChurchWiseAI <hello@auth.churchwiseai.com> | auth.churchwiseai.com |
| Outreach (cold + warm marketing) | varies | mail.churchwiseai.com |
Each subdomain gets its own SPF + DKIM + DMARC chain in Porkbun and its own verification in Resend. Once split:
- The auth subdomain only sends low-volume, high-engagement mail — its reputation rises and stays high.
- The marketing subdomain handles the cold-outreach blast — if it ever gets flagged, magic links keep landing in the inbox.
- The root domain still has SPF / DMARC, but its
include:chain is shorter so it's harder to bust.
Founder action (pending): decide if/when to split. The minimum viable version of this PR ships the plain-text body and the /signin page; the subdomain split is a separate ~1 hr DNS task once Resend dashboard access is in hand.
Action 3 — quick wins inside the templates
Already shipped in this PR:
- ✅ Plain-text alternative on every send.
Still to verify per template (low priority but worth a pass):
- Subject lines ≤ 50 characters.
- At least one
List-Unsubscribeheader on marketing sends (Resend supports theheadersfield — already wired inemail-provider.ts). - No link shorteners.
- Sender display name uses a real person on cold outreach (e.g.,
Pastor John Moelker <john@churchwiseai.com>notChurchWiseAI <hello@churchwiseai.com>). Already done in some outreach paths; audit the rest. - BIMI + VMC for logo display in Gmail / Apple Mail — nice-to-have once DMARC is at
p=quarantineorp=reject.
Action 4 — see it in the real inbox
Use mail-tester.com to spam-score every transactional template before considering this work done. Steps:
- Visit mail-tester.com — copy the unique test address it shows.
- From a deployed preview, trigger each transactional template addressed to that test address:
- Welcome (use the resend-link flow with a known admin email)
- Lifecycle (cron at
/api/cron/lifecycle-emails) - Inbox notification (send a test prayer request to a test premium account)
- Click "Then check your score" — anything below 8/10 needs investigation.
Run the same battery after the subdomain split to confirm auth.churchwiseai.com scores higher than the current single-identity setup.
Founder action items
A small punch list for after this PR merges. Track in FOUNDER_ACTIONS.md.
| # | Action | Owner | Time |
|---|---|---|---|
| 1 | Run the four nslookups above; paste output into a comment on this PR | founder | 5 min |
| 2 | Spam-score the welcome email via mail-tester.com from a deployed preview | founder | 10 min |
| 3 | Decide on the auth/marketing subdomain split (this runbook §Action 2) | founder | discussion |
| 4 | Move DMARC from p=none → p=quarantine after a week of clean Resend reports | founder | 5 min |
| 5 | Add BIMI + VMC once DMARC is at quarantine | founder | 30 min + VMC cost |
Change history
- 2026-05-26 — initial doc, written after Anthony reported every onboarding email landing in junk. Companion to
feat(email): auto-derive plain-text body for every transactional send.