- variables.css: orange primary (#f6821f), dark bg (#0a0a0a), Inter font - layout.css: orange radial glow, unified container 1200px (no width jump) - components.css: orange buttons, remove backdrop-filter on inputs/cards Fixes blurred form fields (double backdrop-filter), jarring width shift between list/table views, and mismatched blue iOS aesthetic vs orange Cloudflare identity of the site. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kill-the-news
Convert email newsletters into private RSS feeds using Cloudflare Workers.
Self-hosted, uses your own domain, and keeps your data in your own Cloudflare account. Live at kill-the.news.
Why this exists
Many newsletters only support email delivery. RSS readers offer a better reading experience, but getting email-only newsletters into RSS usually means relying on shared third-party infrastructure.
kill-the-news keeps the same workflow while avoiding shared domains and shared data stores.
Features
- One-click feed creation from an admin dashboard
- Bulk feed/email deletion from the admin dashboard (safe checkbox-based flow)
- Inline double-confirm delete interactions with toast feedback in the admin dashboard
- Resizable + sortable table columns in the admin dashboard (Table view)
- Unique newsletter addresses per feed (for example
apple.mountain.42@yourdomain.com) - Cloudflare Email Workers ingestion (no third-party service)
- ForwardEmail webhook ingestion with source-IP verification (optional alternative)
- Optional per-feed sender allowlist (
email@domain.comordomain.com) - RSS generation on demand (
/rss/:feedId) - Atom feed at
/atom/:feedId - Email attachments stored in Cloudflare R2 and exposed as RSS enclosures (optional)
- Cloudflare KV storage for feed config + email metadata/content
- Password-protected admin UI
Architecture
Two ingestion methods are supported — pick one or use both:
| Method | How it works |
|---|---|
| Cloudflare Email Workers | Cloudflare Email Routing delivers the raw message directly to the Worker via the email() handler — no outbound webhook needed |
| ForwardEmail webhook | ForwardEmail parses the message and POSTs a JSON payload to POST /api/inbound; the Worker verifies the source IP before processing |
Common path:
- Incoming email arrives at
user@yourdomain.com. - The Worker resolves the feed from the recipient address and stores the email in KV.
https://yourdomain.com/rss/:feedIdrenders RSS from stored items./adminprovides feed management and email deletion.
Main routes:
src/lib/cloudflare-email.ts: Cloudflare Email Workers ingestionsrc/routes/inbound.ts: ForwardEmail webhook ingestionsrc/routes/rss.ts: RSS renderingsrc/routes/atom.ts: Atom feed renderingsrc/routes/files.ts: attachment file serving from R2src/routes/admin.ts: admin UI + feed CRUD
Requirements
- Node.js 20+
- A Cloudflare account (free plan works — Workers, KV, and Email Routing are all included)
- A domain added to Cloudflare as a zone (DNS managed by Cloudflare)
- A ForwardEmail account (Option B only)
Cloudflare setup
If your domain is not yet on Cloudflare: in the Cloudflare dashboard, go to Add a site, enter your domain, choose the Free plan, and follow the instructions to update your nameservers at your registrar. Wait for the zone to become active (usually a few minutes).
Setup
-
Clone this repository.
-
Authenticate Wrangler:
npx wrangler login -
Run setup:
bash setup.shThe script will prompt for an admin password and your domain, then:
- install npm dependencies
- verify Cloudflare auth (
wrangler whoami) - create KV namespaces (
EMAIL_STORAGE+ preview) in your account - set the
ADMIN_PASSWORDsecret in theproductionenvironment - generate
wrangler.tomlfromwrangler-example.tomlwith your KV IDs, domain, and today's compatibility date
-
Configure email ingestion — choose one of the two options below.
Option A — Cloudflare Email Workers (recommended)
No third-party service required. Cloudflare receives the email and hands it directly to the Worker.
- In the Cloudflare dashboard, go to Email → Email Routing for your zone and click Enable Email Routing. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically.
- Under Email Routing → Routing Rules, add a Catch-all rule:
- Action: Send to Worker
- Worker:
kill-the-news(the name fromwrangler.toml)
That's it. No webhook configuration is needed.
Option B — ForwardEmail (alternative)
Use this if you prefer ForwardEmail's additional features (sender filtering, open-tracking, etc.).
Add these DNS records in Cloudflare (DNS → Records):
| Type | Name | Content | Notes |
|---|---|---|---|
| MX | @ | mx1.forwardemail.net |
Priority 10, DNS only |
| MX | @ | mx2.forwardemail.net |
Priority 10, DNS only |
| TXT | @ | "forward-email=https://yourdomain.com/api/inbound" |
webhook target |
| TXT | @ | "v=spf1 include:spf.forwardemail.net -all" |
SPF |
Replace yourdomain.com with your actual domain.
The Worker verifies each webhook request against ForwardEmail's published MX IP list before processing it.
-
Deploy:
npm run deployWrangler will create the Worker and register
yourdomain.com(andwww.yourdomain.com) as custom domains pointing to it. Cloudflare handles TLS automatically. -
Open
https://yourdomain.com/adminand sign in.
Tip: To verify the Worker is running, check Workers & Pages → kill-the-news in the Cloudflare dashboard. The Custom Domains tab should list your domain once the deploy succeeds.
Development
npm install
npm run dev
npm test
npm run build
Configuration notes
wrangler-example.tomlis the template;wrangler.tomlis generated locally.- Keep
compatibility_datefresh when doing runtime upgrades. ADMIN_PASSWORDis a Cloudflare Worker secret, not a plain env var in config.
Feed size limit
By default the worker keeps emails until the feed's stored data exceeds 512 KB, then drops the oldest entries (and their KV records) to stay under the limit. This is more robust than a fixed entry count for HTML-heavy newsletters.
To override the threshold, add to wrangler.toml under [vars]:
FEED_MAX_SIZE_BYTES = "524288" # 512 KB — adjust as needed
Email attachments (R2)
When an incoming email contains attachments, the Worker can store them in a Cloudflare R2 bucket and expose them as <enclosure> elements in the RSS feed (and <link rel="enclosure"> in Atom). Each attachment is served at /files/{id}/{filename} with an immutable cache header.
This feature is optional. If no R2 bucket is bound, attachments are silently ignored and nothing else changes.
Setup:
- Create an R2 bucket in the Cloudflare dashboard (R2 Object Storage → Create bucket), or with Wrangler:
npx wrangler r2 bucket create your-bucket-name - In
wrangler.toml, uncomment and fill in the R2 binding (the commented block fromwrangler-example.toml):Do the same underr2_buckets = [ { binding = "ATTACHMENT_BUCKET", bucket_name = "your-bucket-name", preview_bucket_name = "your-bucket-name-preview" } ][env.production](withoutpreview_bucket_name). - Redeploy:
npm run deploy
Attachments are deleted from R2 automatically when the corresponding email is deleted from the admin UI, or when an email is dropped during feed size trimming.
External auth provider (Authelia / Authentik / reverse proxy)
Instead of the built-in password login you can delegate admin authentication to a reverse proxy that sets a trusted user header (Remote-User or X-Forwarded-User).
Required Worker secrets (set with wrangler secret put, never in [vars]):
| Secret | Description |
|---|---|
PROXY_AUTH_SECRET |
Shared secret between the proxy and the Worker |
Required [vars] in wrangler.toml:
PROXY_TRUSTED_IPS = "10.0.0.1" # comma-separated IPs of your reverse proxy
When both are configured, the Worker authenticates a request if:
CF-Connecting-IPis inPROXY_TRUSTED_IPS- The
X-Auth-Proxy-Secretheader matchesPROXY_AUTH_SECRET Remote-UserorX-Forwarded-Useris non-empty
Password login remains available as a fallback when the proxy check fails.
Security note:
CF-Connecting-IPcan be spoofed on directworkers.devrequests. Disable theworkers.devsubdomain in production (workers_dev = falsein[env.production]).
Security notes
- When using Option B (ForwardEmail), inbound webhook access is IP-restricted to ForwardEmail MX sources.
- Admin auth uses a signed,
HttpOnly,Secure,SameSite=Strictcookie. - Admin responses are
no-storeto avoid cache leakage. - For high-value feeds, set
Allowed sendersso only known sender addresses/domains are accepted. - You should use a strong admin password and rotate periodically.
- All secret comparisons (admin password, proxy secret) use constant-time comparison to prevent timing attacks.
Upgrading dependencies
To refresh dependencies to latest:
npm outdated
npm install
npm test
npm run build
Then update compatibility_date and redeploy.
Acknowledgements
- kill-the-newsletter by Leandro Facchinetti — the inspiration for this project and the reference implementation for feature ideas (Atom feeds, attachment enclosures, entry HTML views, and more).
- Email-to-RSS by yl8976 — the initial codebase this project is based on.
License
MIT