Files
kill-the-news/TODO.md
T
Julien Herr 45d2a14a12 feat(api): add versioned REST API with OpenAPI 3.1 spec
Expose /api/v1/* for feed and email management (feeds CRUD, email
list/get/delete, stats) so the service can be automated without scraping
the admin UI. Built on @hono/zod-openapi; the OpenAPI 3.1 spec is served at
/api/openapi.json with a Scalar reference at /api/docs.

Auth is token-based (Authorization: Bearer <ADMIN_PASSWORD>) plus the
existing reverse-proxy headers — no cookie, no CSRF. Extracted the auth
primitives into src/lib/auth.ts and the feed create/update/delete
orchestration into src/lib/feed-service.ts so the admin UI and the REST API
share a single source of truth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:01:15 +02:00

6.6 KiB

TODO

Feature gaps identified by comparing with kill-the-newsletter.

Quick wins

  • Author field in RSS entries — expose the from address as <author> in each RSS <item>. The value is already stored in KV, just not rendered in the feed XML.

  • HTML view for individual entries — serve each email as an HTML page at e.g. /entries/:feedId/:timestamp. Useful for reading emails outside a feed reader and for debugging. kill-the-newsletter serves these at /feeds/{feedId}/entries/{entryId}.html with a Content-Security-Policy header.

  • JSON API for feed creation — accept Content-Type: application/json on POST /admin/feeds and return { feedId, email, feedUrl }. Useful for automation (e.g. Tofu/OpenTofu provisioning).

  • Project favicon — serve a single bundled icon at /favicon.ico and add a <link rel="icon"> in the shared Layout so the admin UI, status page, and entry views stop 404-ing. Doubles as the default/fallback icon for the per-feed favicon feature below.

Medium effort

  • Size-based feed trimming — instead of a fixed 50-entry cap, drop the oldest entries when the feed exceeds a size threshold (kill-the-newsletter uses ~512 KB). More robust for HTML-heavy newsletters where one entry can dominate.

  • Atom feed format — expose feeds as Atom (application/atom+xml) in addition to or instead of RSS 2.0. Atom has better native support for HTML content and author metadata.

  • Authelia / external auth provider support — allow delegating admin authentication to an external identity provider (e.g. Authelia, Authentik) via a trusted header (Remote-User, X-Forwarded-User) set by a reverse proxy. The Worker would accept the header as proof of authentication instead of checking the cookie, with a configurable secret or IP allowlist to trust only the proxy.

  • Per-feed favicon from the last sender's domain — give each feed an icon by fetching the favicon of the last sender's domain, so feeds are visually distinguishable in readers and the admin UI. Resolve the domain from the most recent email's from, fetch its favicon (e.g. https://<domain>/favicon.ico or a parsed <link rel="icon">, with a fallback service), and cache the result aggressively (KV/R2 + Cache API with a long TTL) so it isn't re-fetched on every request. Expose it via the RSS <image> / Atom <icon> and the admin feed list.

  • RFC 8058 one-click unsubscribe on feed deletion — when a feed is deleted, automatically unsubscribe from the newsletters that fed it so messages stop arriving at the now-dead address. Parse and store the List-Unsubscribe / List-Unsubscribe-Post headers (RFC 8058) from incoming emails, then on deletion POST List-Unsubscribe=One-Click to each stored unsubscribe URL. Requires capturing the headers during ingestion (src/lib/email-processor.ts) and firing the outbound requests from the feed-delete paths (src/routes/admin/feeds.tsx), ideally via ctx.waitUntil.

Heavy

  • Email attachments as RSS enclosures — store attachments in Cloudflare R2 and expose them as <enclosure> elements in the feed. kill-the-newsletter serves them at /files/{enclosureId}/{filename}.

  • WebSub (PubSubHubbub) push notifications — notify subscribers in real time when a new email arrives, instead of requiring them to poll the feed. Requires either integrating a public WebSub hub or implementing the hub protocol directly.

  • Rate limiting via Cloudflare WAF rules — protect /api/inbound and /admin against abuse. Configure WAF custom rules in the Cloudflare dashboard (or via Terraform): rate-limit /api/inbound to ~60 req/min per IP, and /admin to ~20 req/min per IP. No code changes required; this is pure infrastructure configuration.

  • REST API with OpenAPI description — expose a documented, machine-consumable REST API for feed/email management (create/list/update/delete feeds, list/read/delete emails, read stats) so the service can be automated without scraping the admin UI. Implemented as a versioned /api/v1/* surface (Bearer-token auth with the admin password, plus the existing proxy-auth) built on @hono/zod-openapi; the OpenAPI 3.1 spec is served at /api/openapi.json with a Scalar docs page at /api/docs. Feed create/update/delete logic was extracted into src/lib/feed-service.ts so the admin UI and the REST API share a single source of truth.

  • Migrate feed metadata to Durable Objects for atomic writes — the current KV-based metadata store has a read-modify-write race condition: two concurrent emails to the same feed can silently overwrite each other's changes. Cloudflare Durable Objects serialise access per feed and eliminate the race entirely. Requires replacing feed:<feedId>:metadata KV writes in src/lib/email-processor.ts with a Durable Object that exposes an appendEmail() RPC, updating wrangler.toml with a DO binding, and migrating existing metadata at deploy time.

Per-feed favicon — design notes

Breakdown of the "Per-feed favicon from the last sender's domain" item above. Goal: each feed shows an icon derived from its newsletter source, fetched once and cached so it never re-fetches on a normal request.

  • Resolve the sender domain — on ingestion, extract the domain from the latest email's from address (extractEmailDomain in src/utils/favicon-fetcher.ts) and persist it as iconDomain on the feed metadata so the icon tracks the most recent sender.

  • Fetch the favicon — resolve an icon URL for the domain: try https://<domain>/favicon.ico, then fall back to https://icons.duckduckgo.com/ip3/<domain>.ico. Runs async via ctx.waitUntil so it never blocks email processing.

  • Cache aggressively — store the fetched bytes (base64) keyed by domain in KV with a 1-week TTL (ICON_TTL_SECONDS). The domain is the cache key so feeds from the same sender share one fetch; the fetch only fires when the cache entry is absent/expired.

  • Serve endpointGET /favicon/:feedId returns the cached bytes with the correct Content-Type and a long Cache-Control, falling back to the project favicon when no domain icon is found.

  • Expose in outputs — the icon is referenced from the RSS <image> and Atom <icon>/<logo> in src/utils/feed-generator.ts, and rendered next to each feed in the admin list/table (src/routes/admin.tsx).

  • Failure handling — missing/blocked favicons degrade gracefully to the project favicon fallback (negative cache entry); icon fetch errors never surface to ingestion or feed rendering.