Host your own email and enhance your privacy
The 2.x roadmap introduces RSS as a first-class feature alongside email. The vision is a cloud-managed feed reader where Cabalmail keeps every subscribed feed up to date on behalf of the user, tracks per-user read/unread/favorite state, applies user-defined filtering and organization, and pushes notifications for selected feeds. Clients (React admin, iOS, macOS, Android) consume a Cabalmail-native API and do not need to implement RSS/Atom parsing themselves.
This document is the requirements pass. It states goals, non-goals, and
confirmed scope, then enumerates the decision points the operator
resolved. Each decision lists the options, trade-offs, and a recommended
starting position; the operator’s inline decisions follow the
recommendations. The companion
rss-implementation-plan.md (modeled on
the existing plans — see
bimi.md,
push-notifications.md) sequences
the work.
This directory is 2.0.x per the operator’s answer to Open Q1; the
release version is 2.0. The plan splits the work across multiple patch
versions (2.0.0, 2.0.1, …).
Revised (2026-09-09): the release is 1.x, not 2.0. The RSS API
is purely additive, which compatibility.md
classifies as a minor release; both RSS docs moved from docs/2.x/ to
docs/1.x/. Each implementation phase ships as its own 1.x minor
release. Further same-day rulings on the plan’s challenges appear as
Revised decision (2026-09-09) annotations under D1, D4, D6, and
D15 below.
After the operator’s inline decisions were recorded, a phase-1 design
pass surfaced cost-shape findings that revised the data-layer choice and
added one new requirement. The original decisions remain inline as
historical record; Revised decision annotations capture the current
state. The companion
rss-implementation-plan.md reflects
the revised state throughout.
These items are baseline. The decisions below add to or refine them.
The decisions are grouped: architecture (1-4), scope cuts (5-8), operational posture (9-10), per-feature design (11-17). Each is independently answerable; some interact and those interactions are called out.
Question. When N users subscribe to the same public feed (e.g. NYT), does Cabalmail fetch it once for all of them or N times?
Options.
Recommendation: B. It is the architecture every mature aggregator (Feedly, NewsBlur, Miniflux, FreshRSS) converges on. The data-model cost of separating “feed” from “subscription” is the same cost you would pay later if you started with A and outgrew it. C is appealing for simplicity but the cache layer ends up doing most of B’s work without B’s clarity.
Decision: B.
Sub-decisions if B is chosen.
What’s the canonical-URL normalizer? (Trailing slashes, scheme, www.
prefix, tracking query params can each produce N copies of “the same”
feed if not normalized.)
Decision: Trailing slashes for URLs provided without paths. Trailing slashes where two users provide an otherwise identical URL and one includes a trailing slash and the other does not. Different query parameters count as different feeds, but order of query params should be normalized. www.
Revised decision (2026-09-09): the apex form is canonical in
every case; the www.example.com example above is superseded
(https://www.example.com/ normalizes to https://example.com/,
matching the example.co.uk example). An http:// input is silently
upgraded to https://; if the feed is not available over https, the
user is told and nothing is fetched.
Revised decision (2026-09-09), trailing slashes: / is canonical
only immediately after the host (and port, if present). Elsewhere it is
up to the server whether a slash belongs: Cabalmail cannot know whether
dir is a directory or some other object, so the path is stored and
fetched exactly as given, and the /dir vs /dir/ examples above are
superseded. A publisher that treats the two as one object redirects,
and the subscribe path canonicalizes the permanent-redirect target.
Revised decision (2026-09-10), the host decides: the rulings above
were stated too strongly. The publisher’s host decides what is
canonical; Cabalmail’s own preferences (www or no www, trailing /
or none) apply only when the host serves both forms. Where it serves one
form, that form is canonical, whatever the user typed. Found on the
first OPML imports: two publishers serve their feed only on www and
answer the apex path with a 404 or a redirect to the front page, so the
apex-first normalizer sent the fetcher to a host that had nothing. As
built: the apex form is tried first; when it does not yield a feed, the
www form is tried once and, if it parses, becomes the canonical URL,
and a permanent redirect from the apex form to the www form of the
same URL keeps www rather than normalizing back to the apex.
Question. The initial brainstorm proposed implementing the folder hierarchy in Dovecot. Should we?
Options.
Recommendation: A. Feed items are mutable (publishers edit and re-publish articles), and the metadata we want (source feed, fetch error, canonical URL, extracted full text, score, original-vs-extracted HTML) does not fit IMAP flags or message structure. The IMAP append-only model fights this domain. B inherits all of C’s problems with none of the leverage. The folder hierarchy concept is independent of the storage engine and is cheap to build natively. C has the further wrinkle that “favorite” and “read” are per-user-per-item state and IMAP flags are per-mailbox; you would end up modeling them outside IMAP anyway.
If a future version wants IMAP access to feeds (the way Gmail exposes labels as IMAP folders), expose it as a read-only IMAP view layered on the native model. Don’t put the source of truth there.
Decision: A.
Question. Should the Cabalmail RSS API be compatible with an existing standard so existing readers (Reeder, NetNewsWire, Unread, FeedMe) work out of the box?
Options.
Recommendation: D for v1, with a clear path to B in a 2.x point release. Locking down a compatibility API on day one slows v1 design; ignoring it forever cuts off a meaningful audience. Designing the native API with this option in mind keeps the door open at low cost.
Trade-off to weigh. Cabalmail’s selling point is the integrated client experience (BIMI, in-house Apple apps, etc.). Inviting third-party clients dilutes that, but for RSS — where the third-party ecosystem is mature and good — interoperability may be a stronger draw than UI consistency.
Decision: A. Justification:
Question. How long do feed items live, and what bounds the storage?
Options.
Recommendation: D, with operator-tunable defaults and a per-user override. Without a cap, storage compounds forever and the DB becomes the bottleneck. Favorites must be exempt or the favorite feature is a lie. A per-feed user override lets power users keep low-volume feeds forever.
Decision: In-feed content is kept forever. Linked content is cached for N days where N is an operator-tuned setting.
Sub-decision. When an item is dropped, does the per-user “read” state go with it (storage win) or persist as a tombstone keyed by item-GUID so a re-fetched item does not pop up as unread again (correctness win)? Recommend the tombstone — small per-row cost, eliminates a class of annoying bugs.
Decision: tombstone.
Revised decision (2026-09-09): “kept forever” applies while a feed has at least one subscriber. When the last user unsubscribes from a feed, its content can be deleted. A cold-storage option for departed feeds may be considered in a future release.
Question. OPML is the standard way users move feed lists between readers. v1 scope?
Recommendation: yes, both in v1. Onboarding without OPML import means every prospective user adds feeds one at a time, which kills adoption. Export is the corresponding exit story — without it, Cabalmail is a roach motel for feed lists. Both are small to implement.
Decision: OPML is in scope.
Open sub-decision. Import behavior on collision — additive with duplicate-feed detection by canonical URL, or replace? Recommend additive.
Decision: Additive.
Question. Many feeds publish only summaries. Should Cabalmail fetch the linked article server-side and extract the body so the user reads the full piece without leaving the reader?
This decision interacts with two confirmed-scope items:
Options.
Recommendation: A in v1, evaluate moving to B in v1.1. Server-side extraction is the single biggest UX win of any modern reader. It is also the same plumbing as the deferred “broken-URL fixer” idea — a server-side transform pipeline keyed on feed identity. Shipping the pipeline behind an opt-in toggle in v1 means the infrastructure is built; flipping the default later is a settings change.
Decision: C. Attempting server-side extraction opens up too many potential rendering issues. If possible, I’d like the embedded web view to support reader mode. My current preferred feed client is Reeder, which seems to embed Apple’s WebKit and supports Apple’s reader view feature.
Operator clarification needed. Confirm which interpretation you meant for “show article by default” in the original scope, so this decision lands on a coherent UX rather than a contradictory mix.
Clarification: “show article by default” means, when a user selects a feed item from a channel, the client opens the linked article in an embedded web view. I prefer this approach not only due to the way it sidesteps rendering and fetching issues, but also because it assures that at the moment I open it, I’m getting the latest version with any corrections that might have occurred since initial publication.
Confirmed (2026-09-09): neither WKWebView nor Android WebView
exposes the browser’s reader mode to apps, so “reader view” is
delivered by injecting Readability.js into the loaded page and
restyling the result. The operator confirmed this satisfies the
requirement and reserves the right to request tweaks after it ships.
Implementation cost. Trafilatura, Mercury Parser, or Readability.js via Node all work. Lambda-friendly with a modest cold-start budget. Publishers’ anti-bot defenses are the main operational risk; some sites will refuse extraction and need to fall back to “open the page.”
Question. A growing share of users’ “feeds” arrive as newsletters. Should Cabalmail provision a magic address that ingests newsletters into the RSS pipeline?
Options.
Recommendation: A. This is the differentiator. An email-platform-with-RSS
that bridges newsletters and feeds is a feature no standalone RSS reader can
match without operating its own SMTP infrastructure. Plumbing reuses
SMTP-IN, the cabal-addresses table, and the per-user mailbox model.
Cost is mostly UI and the rule that routes mail-to-a-newsletter-address into
feeds rather than the inbox.
Decision: B. The only newsletters I currently subscribe to are from Substack, which also supports feeds (RSS or Atom or both, not sure). I generally ignore the emailed version and rely on the RSS feed. (The concern for corrections is particularly sharp with email because publishers cannot retract messages already in recipients’ inboxes.) And As I say, I’m building the feed reader that I want primarily. But I’m fine with keeping it on the roadmap for other users.
Sub-decision. One newsletter address per user with sender-based routing
into feeds, or one address per newsletter-feed? Recommend per-feed
addresses, generated on demand, revocable like normal Cabalmail addresses.
Sender-based routing is fragile (newsletter From: addresses change without
notice; same From: can carry multiple newsletter products).
Decision: deferred. If we decide to move forward after the initial release, we can revisit this issue. Today, I’m inclined to agree with the on demand approach, but let’s not consider that locked in unless/until we decide to move forward with the feature.
Question. Inverse of Decision 7 — should a low-volume feed be deliverable as a daily/weekly email digest into the user’s mailbox?
Recommendation: defer to v1.1. Useful, the plumbing exists, but it adds UI and scheduling surface that v1 doesn’t need. Worth noting now so the v1 storage model doesn’t preclude it (it shouldn’t — digest generation is a pure read from the item store).
Decision: No. I don’t see this feature adding enough UX value to justify the effort. I’ve already commented on the rendering challenges. This will be even harder for inbox viewing where Javascript is generally not available.
These are checklist items the implementation plan must cover. Listed here so they don’t get lost.
Cabalmail/2.0 (+https://cabalmail.com/feedbot) with an info page at
that URL explaining what the bot does, how to block it, and how to
contact the operator. Publishers expect this.If-Modified-Since and If-None-Match
based on the prior fetch’s response. Non-negotiable for politeness and
bandwidth.Decision: Cabalmail wants to be a good Internet citizen and honor all conventions regarding polite, ethical, and responsible use of others’ resources. I think that at least gestures at answers all of these. More specific guidance can be elucidated at implementation time.
Question. When a feed dies (404, malformed XML, certificate expiry, redirect loop), how does the user find out?
Options.
Recommendation: B in v1, C as opt-in in v1.1. Visibility is table stakes; proactive notification needs care not to become noise (a feed flapping between 200 and 503 shouldn’t notify on every flip).
Decision: B. I’m skeptical of even wanting C. I think an in-app message ought to be enough. I might reconsider after using the product for a while.
Question. Which authentication schemes for private feeds do we support in v1?
Options.
https://example.com/feed?key=abc123). This requires no auth-storage
code per se, but should be flagged as a security/privacy item (the key
is in our DB; revoking access means rotating the feed URL on the
publisher’s side).Recommendation: C in v1. Basic covers the easy case; URL-embedded keys cover the next-easiest; cookies are a modest extension. OAuth deferred to a follow-on with its own design pass.
Decision: C. Additional cookie-handling requirements:
Storage. Credentials in SSM SecureString, KMS-encrypted, keyed by
(user, feed_id). Consider whether shared canonical feeds (Decision 1B)
inherit credentials from the first subscriber or whether each subscriber
stores their own — recommend the latter (no cross-user credential leakage
risk) which implies credentialed feeds bypass the shared-fetcher path.
Decisions: No shortcuts with user credentials. Only public feeds get shared fetching.
Question. Notifications are per-feed opt-in in the confirmed scope. Should the opt-in be available at per-folder level too? What’s the default for a freshly subscribed feed?
Options.
Recommendation: B with default off. Two levels of granularity match how users naturally think about it (“everything in this folder is important”). Default off is safer than the alternatives and the push-budget math is friendlier. C is clever but surprising — users will be confused why some feeds notify and others don’t.
Decision: A for v1.0, B on the roadmap.
Question. Articles embed images hosted on publishers’ CDNs. What does Cabalmail do with them?
Options.
<img> URLs rewritten through a Cabalmail
proxy. Publisher only sees Cabalmail’s IPs. Reduces tracking; improves
privacy.Recommendation: C, with a modest cache TTL (7 days?). The privacy win is significant — feed readers are routinely targeted with tracking pixels — and the operational cost is bounded. The cache also makes the retention story (Decision 4) more honest: an article kept for 90 days won’t be a broken-image graveyard.
Cost. Bandwidth and S3 storage. Worth a back-of-envelope estimate before committing — if a typical article has 5 images at 200 KB each, a user with 50 articles a day at 90-day retention pulls 4.5 GB of image cache. Tractable, but not free.
Decision: C with 7 day TTL.
Confirmed scope includes filter by read/favorite/all. Open questions:
Decisions:
Revised decision (after design exploration): Server-side full-text search deferred indefinitely (the Aurora-vs-DynamoDB cost comparison under Open Q2 made Postgres tsvector too expensive at hobby scale, and OpenSearch is worse). The operator confirmed cross-feed FTS is not a real-world need. Per-feed FTS moves to Apple clients only, backed by a local SQLite/FTS5 item cache that doubles as the offline-reading store (see new Decision 18). React has no search in v1; this is a documented limitation. Cross-feed search remains out. The “per-feed keyword mute” and “tagging” decisions above are unchanged.
| Feature | Description | v1 recommendation | Decision |
|---|---|---|---|
| Mark-all-as-read | Per feed, per folder, global | In — table stakes | In |
| Save-for-later | Reading-intent flag distinct from favorite | Defer | Defer |
| Snooze | Hide article until time T | Defer | Out |
| Keep-unread pin | Prevent auto-mark-read on a specific item | Defer | Marking as read should be manual by default. Auto-mark-as-read should be a per-user optional setting (not per-feed). |
| Auto-mark-read on scroll | Vs. explicit only | In, user-toggle, default off | Defer |
| Cross-feed dedup | Same article syndicated in N feeds appears once | Defer to v1.1 | Out |
| Reading-time estimate | Per article, from extracted text | In — trivial | In — make it optional and off unless user opts in (want to avoid distracting UI elements) |
Revised decision (2026-09-09): the mail clients no longer support
time-delayed mark-as-read; the choice is manual or
automatic-and-immediate (on open). Feeds offer the same two options,
under their own rss_mark_as_read preference rather than the mail
mark_as_read key.
Revised decision (2026-09-09), reading-time estimate: dropped from v1 and kept as future work. With D6 = C there is no extracted text to estimate from, so a v1 estimate would be wrong for every summary-only feed.
Trivial to add alongside RSS/Atom; the parser library choice usually covers all three. Recommendation: in v1.
Decision: In
What is the default fetch cadence for a feed? Are per-user overrides allowed?
Recommendation: default cadence of every 60 minutes; an operator-configurable “high-velocity” tier (every 15 minutes) for major news sources; per-user override bounded to operator-set min/max. Cadence is per-feed not per-user (a shared canonical feed under Decision 1B has a single fetch cadence — the maximum of any subscriber’s requested cadence, clamped to the operator’s bounds).
Decision: I’d like the server to assess feed velocity over time and dynamically adjust cadence within operator-set bounds. I don’t want to expose cadence settings to end users.
Added after design exploration; not present in the original decision set.
Question. The local item cache that backs Apple-side per-feed FTS (see Revised decision under Decision 14) naturally also enables offline reading. Should offline reading be a first-class requirement?
Decision: yes. The operator uses offline reading regularly on the
current reader (Reeder) and expects parity. Offline reading covers
in-feed content (summary plus any content_html the feed delivered).
Articles opened in the embedded web view still require network — per
Decision 6 we do not extract or cache article bodies server-side, and
the WKWebView’s article rendering is a live publisher fetch. Images
inside in-feed content rely on whatever the embedded URLCache happens
to have picked up from prior online viewing; explicit aggressive
pre-cache of image bytes is deferred.
Offline mutations (mark-as-read, favorite/unfavorite) are queued in a small local pending-mutations table and dispatched to the server on reconnect with last-write-wins semantics. Cache retention defaults and the cross-device inconsistency story are implementation details captured in the companion plan.
These exist to keep doors open. They are not optional; they constrain the v1 data model and are cheap if done up front, expensive if retrofitted.
(user, item_id) -> {read, favorite, ...}
table is the keystone of the storage model.(Two items previously listed here are moot under the operator’s decisions: a tombstone table — D4 chose “items kept forever,” so items are never dropped — and a per-feed server-side transform pipeline — D6 chose option C, no server-side extraction or rewriting. The implementation plan does not include either.)
compatibility.md classifies that as a minor release; the docs moved
from docs/2.x/ to docs/1.x/.CabalmailKit; the React app is
independent of both. Android will likely lag (still in 1.1.x
roadmap).
Decision: Phased rollout is fine. 2.0.x has plenty of unused values for x.
Revised decision (2026-09-09): Apple first, Android second; Linux
waits until its mail client has caught up. There is no React RSS
client (the React app receives no new features).The companion rss-implementation-plan.md
sequences the changes into shippable phases, names the modules and
services, identifies the data model and API shape, and lays out the
migration path from “no RSS in 1.x” to “RSS GA in 2.x”. It reflects the
Revised decision annotations above (DynamoDB primary store; per-feed
FTS on Apple clients only; offline reading on Apple as a first-class
requirement).