Email Deliverability Setup — Custom Domain with SPF, DKIM, DMARC
1. Overview
Current setup: Resend with default shared sending domain (noreply@agentix.app).
Target: Custom sending subdomain (mail.agentix.app) with full email authentication — SPF, DKIM, and DMARC.
Why this matters:
- Custom domain improves deliverability and inbox placement
- Prevents transactional emails from being classified as spam
- SPF/DKIM authenticate that emails genuinely originate from Agentix
- DMARC adds a policy layer with aggregate reporting for monitoring
- Verification email (signup)
- Password reset email
2. Prerequisites
- Resend account with API key (
RESEND_API_KEYalready configured) - DNS access for the
agentix.appdomain (registrar or DNS provider) - Railway environment variable access (to update
FROM_EMAIL)
3. Step 1 — Add Domain in Resend
- Navigate to Resend Dashboard > Domains > Add Domain
- Enter:
mail.agentix.app- Use a subdomain (
mail.), not the root domain — keeps the root domain clean for website/corporate email
- Use a subdomain (
- Select region (use the same region as your Resend API key)
- Resend will generate DNS records to add (SPF TXT + DKIM CNAMEs)
4. Step 2 — Configure SPF (Sender Policy Framework)
SPF tells receiving mail servers which IP addresses are authorized to send email on behalf ofmail.agentix.app.
Resend provides the SPF record when you add the domain. Add the DNS TXT record:
| Field | Value |
|---|---|
| Host | mail.agentix.app (or mail depending on DNS provider) |
| Type | TXT |
| Value | The SPF record Resend provides (typically v=spf1 include:amazonses.com ~all since Resend uses AWS SES) |
| TTL | 3600 (1 hour) or DNS provider default |
5. Step 3 — Configure DKIM (DomainKeys Identified Mail)
DKIM adds a cryptographic signature to outbound emails, proving the message was not altered in transit and genuinely originated from the domain. Resend generates three CNAME records for DKIM signing. Add all three DNS CNAME records exactly as shown in the Resend dashboard:| Field | Value |
|---|---|
| Host | resend._domainkey.mail.agentix.app (or as shown by Resend) |
| Type | CNAME |
| Value | Points to Resend’s DKIM key servers |
6. Step 4 — Verify Domain in Resend
- Return to Resend Dashboard > Domains
- Click Verify next to
mail.agentix.app - All records should show green checkmarks (SPF TXT + 3 DKIM CNAMEs)
- If verification fails:
- Double-check DNS records for typos
- Wait longer for propagation
- Some DNS providers add the root domain automatically (e.g., entering
mailbecomesmail.agentix.app)
7. Step 5 — Configure DMARC (Domain-based Message Authentication, Reporting & Conformance)
DMARC ties SPF and DKIM together with a policy that tells receiving servers what to do with unauthenticated mail, and where to send aggregate reports. Add the DNS TXT record:| Field | Value |
|---|---|
| Host | _dmarc.mail.agentix.app (or _dmarc.mail depending on DNS provider) |
| Type | TXT |
| Value | v=DMARC1; p=none; rua=mailto:dmarc-reports@agentix.app; pct=100 |
| TTL | 3600 (1 hour) or DNS provider default |
p=none (monitoring mode) — this collects reports without rejecting any emails. This is critical for safely rolling out DMARC.
Verify locally:
v=DMARC1; p=none; rua=mailto:dmarc-reports@agentix.app; pct=100
8. Step 6 — Update FROM_EMAIL Environment Variable
Once the domain is verified in Resend:- In Railway dashboard > API service > Variables:
- Change
FROM_EMAILfrom: - To:
- Change
- Redeploy the API service for the change to take effect
- No code changes needed —
auth.tsreadsprocess.env.FROM_EMAILat runtime
9. Verification Checklist
After completing all steps, verify end-to-end:- Resend dashboard shows
mail.agentix.appdomain as Verified -
dig TXT mail.agentix.appreturns the SPF record -
dig CNAME resend._domainkey.mail.agentix.appreturns the DKIM CNAME -
dig TXT _dmarc.mail.agentix.appreturns the DMARC record - Send a test email (trigger password reset or signup verification flow)
- Check email headers in Gmail: Show Original > look for:
spf=passdkim=passdmarc=pass
- Use mail-tester.com to score deliverability (aim for 9+/10)
10. Troubleshooting
SPF fails
- Check TXT record is on the correct subdomain (
mail.agentix.app, notagentix.app) - Verify you used exactly the SPF value Resend provided
- Check for duplicate TXT records on the same subdomain
DKIM fails
- Ensure all three CNAME records are added exactly as shown in Resend
- Check for trailing dots in CNAME values (some DNS providers require them, others don’t)
- CNAME records can take longer to propagate than TXT records
DMARC fails
- Ensure
_dmarcprefix is correct for your DNS provider - Some providers need
_dmarc.mail(without the root domain appended) - Verify no conflicting DMARC record exists on the parent domain
Emails still going to spam
- Check if domain is on any blacklists: mxtoolbox.com/blacklists
- Verify email content doesn’t trigger spam filters (avoid ALL CAPS, excessive links)
- Check that
Reply-Toheader is set (some spam filters penalize no-reply addresses) - Allow 24-48 hours after DNS changes for reputation to build
11. DMARC Policy Upgrade Path
DMARC enforcement should be rolled out gradually:| Timeline | Policy | DNS Value | Effect |
|---|---|---|---|
| Week 0 (now) | p=none | v=DMARC1; p=none; rua=mailto:dmarc-reports@agentix.app; pct=100 | Monitor only — collect reports, don’t reject |
| Week 2-4 | Review | — | Check DMARC aggregate reports at dmarc-reports@agentix.app |
| After clean reports | p=quarantine | v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@agentix.app; pct=100 | Suspicious mail goes to spam |
| After 2 more weeks clean | p=reject | v=DMARC1; p=reject; rua=mailto:dmarc-reports@agentix.app; pct=100 | Unauthenticated mail is rejected |
- Reports arrive as XML attachments to
dmarc-reports@agentix.app - Use a DMARC report analyzer like dmarcian.com or postmark DMARC
- Look for: sources sending as your domain, SPF/DKIM pass rates, any unauthorized senders
References
- Resend Domains Documentation
- SPF Record Syntax
- DKIM Explained
- DMARC.org
- Code reference:
apps/api/src/lib/resend.ts(email client with domain validation) - Environment:
apps/api/.env.example(FROM_EMAIL variable)