This project is born out of the frustration against expensive DMARC analytics services that individuals can't afford.
A self-hosted DMARC report analyzer on Cloudflare's free tier. One deploy script sets up everything: email ingestion, database, dashboard, and access control.
Email Sender → Cloudflare Email Routing → Worker email() → D1 (SQLite)
Worker fetch() → Dashboard (Svelte)
Cloudflare Access (login wall)
A single Cloudflare Worker handles both DMARC report ingestion (via email) and serves the dashboard + API. Cloudflare Access protects the entire application.
- Node.js (v18+)
- pnpm
- A Cloudflare account with a domain (free plan works)
- A Cloudflare API token with permissions:
- Workers Scripts: Edit
- D1: Edit
- DNS: Edit
- Email Routing Rules: Edit
- Access: Apps and Policies: Edit
- Access: Groups: Edit (only required if
ACCESS_ALLOWED_IPSis set)
-
Enable Email Routing for your receiving domain:
Cloudflare Dashboard → your domain → Email → Email Routing → Enable
Note: Cloudflare Email Routing requires your receiving domain's MX records to point to Cloudflare's mail servers. If your domain is already using another email provider, you cannot use Cloudflare Email Routing on the same domain without replacing the current MX records, which would break your existing email setup. A cheap dedicated domain is the recommended solution.
-
Enable Cloudflare Access on your account:
Cloudflare Dashboard → Access → Get started
Both are one-time settings best configured through the dashboard.
git clone <this-repo>
cd dmarc-dashboard-kit
pnpm install
pnpm run deployOn first run, pnpm run deploy creates a .env file for you to fill in:
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_ZONE_ID=
DMARC_EMAIL=dmarc
REPORT_AUTHORIZED_DOMAINS=
ACCESS_ALLOWED_EMAILS=
ACCESS_ALLOWED_EMAIL_DOMAINS=
ACCESS_ALLOWED_IPS=Then run pnpm run deploy again.
| Step | What |
|---|---|
| 0 | Auto-detects domain from zone ID |
| 1 | Creates D1 database (dmarc-reports) |
| 2 | Generates wrangler.toml with D1 database ID |
| 3 | Runs D1 schema migrations (all migrations/*.sql in order) |
| 4 | Builds Svelte dashboard |
| 5 | Deploys worker (API + dashboard + email handler) |
| 6 | Creates email routing rule + _report._dmarc DNS TXT records |
| 7 | Creates Cloudflare Access application + policy |
To monitor a domain, two DNS records are needed:
1. On the monitored domain:
_dmarc.otherdomain.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"
2. On your receiving domain (authorizes accepting reports):
otherdomain.com._report._dmarc.yourdomain.com TXT "v=DMARC1;"
The deploy script handles step 2 automatically:
- Set
REPORT_AUTHORIZED_DOMAINS=otherdomain.com,another.orgin.envfor specific domains - Or leave it empty to create a wildcard
*._report._dmarcthat accepts reports from any domain
Warning:
p=noneonly monitors — it does not block spoofed emails. Once you've reviewed your DMARC reports and confirmed all legitimate senders pass DKIM/SPF, move top=quarantineand thenp=rejectas soon as possible.
# Run D1 migrations locally
pnpm run migrate:local
# Start worker dev server (API + email handler)
pnpm run dev
# In another terminal — start dashboard dev server
cd dashboard
pnpm install
pnpm run devThe dashboard dev server (Vite) runs separately during development. In production, the built dashboard is served as static assets by the worker.
├── src/ # Cloudflare Worker
│ ├── index.ts # Entry: email() + fetch() handlers
│ ├── email-handler.ts # Email → parse XML → D1
│ ├── api.ts # /api/* routes
│ ├── db.ts # D1 queries
│ └── types.ts # TypeScript types + DMARC enums
├── dashboard/ # Svelte SPA
│ └── src/
│ ├── App.svelte
│ ├── components/ # SummaryCards, Charts, Tables
│ └── lib/api.ts # API client
├── migrations/
│ ├── 0001_init.sql # D1 schema (reports + record_rows tables)
│ └── 0002_record_rows_unique.sql # unique index on record_rows
├── deploy.sh # One-command deploy
└── wrangler.toml # Generated by deploy.sh (gitignored)
MIT — see LICENSE. Portions adapted from Cloudflare's dmarc-email-worker.
