Commonfeed is a local-first reader for finite, purpose-driven sessions. It loads current material from RSS or Atom sources you choose, compiles a session with a stopping point, explains every selection, stops, and asks whether it delivered. Your sources, feedback, and receipts never leave your machine; the only server is the one running on it.
Commonfeed is a local reader with a published package, tests, and a release workflow. It remains a focused product probe; this README does not establish long-term reliability or improved reading outcomes. There are no accounts or cloud sync. The sample session is fictional and is not evidence of live feed quality.
Requires Node.js 22 or newer and npm.
npx commonfeedThat starts the local server and opens your browser. To keep it installed:
npm install -g commonfeed
commonfeedTo run the exact source in this repository:
git clone https://github.com/zacharymplace/commonfeed.git
cd commonfeed
npm ci
npm startKeep the terminal open; press Ctrl+C to stop. Options: --port <n>, --host <address>, --no-open, or the PORT and HOST environment variables. The server binds to loopback unless you choose otherwise; binding 0.0.0.0 exposes the feed proxy to your network.
npm run dev starts the server without opening a browser. npm run release:ready is the completion gate: the syntax check and the full test suite. npm run release rehearses a release against the last commit.
- Open the local address printed in the terminal.
- Before adding sources, choose Or try a sample session first.
- Move through the five fictional items, inspect the selection reasons, and reach Session complete. The sample saves no receipt, signals, or seen marks.
- To try real material, add an RSS/Atom feed or a publication homepage, choose an intention and session length, and start a session. Real feeds need network access; publisher availability and content vary.
Expected startup line at the default port:
Commonfeed running at http://127.0.0.1:4173
A second line identifies the data location or browser-only fallback. Real session receipts record planned versus actual time, previewed items, and your explicit feedback. They describe activity, not an independently verified learning outcome.
server.mjs is the local server (feeds, discovery, articles, static files). The browser side is app.js, which runs boot.js; that wires ui.js (state, timers, persistence, render loop), store.js (everything in local storage), sources.js (feeds, discovery, OPML), reader.js (article mode), and the three screen-*.js modules. data-store.js is the server side of your data file and store.js the browser side. feed-engine.js holds the ranking, receipts, pace, and summaries and has no DOM dependency. Every *.test.js runs under node --test.
Your sources, saved items, feedback, seen marks, and receipts are kept in one file, ~/.commonfeed/data.json, readable only by you. The browser holds a working copy, and the file wins whenever the two differ, so a different browser, a different port, or cleared site data costs you nothing. --data <directory> or COMMONFEED_DATA moves it; --data off keeps everything in the browser only, as does any location the server cannot write to.
The start screen has a "Your data" panel that says where things are kept and offers a full backup, a restore that replaces everything with the backup's contents, and an erase. OPML export remains for moving just your source list to another reader.
The local server only answers requests addressed to this machine (localhost, 127.0.0.1, [::1]), which stops a page on another site from reaching it by pointing a domain at your loopback address. If you bind another address and reach it by name, list that name in COMMONFEED_ALLOWED_HOSTS. Changes to the data file are accepted only from the page itself.
docker build -t commonfeed .
docker run --rm -p 127.0.0.1:4173:4173 -v commonfeed-data:/data commonfeedThen open http://localhost:4173. The named volume holds your data file, so it outlives the container.
The interface follows your system light or dark preference. Inside a session, the right and left arrow keys move between items, 1 and 2 give attention feedback, S saves the item, R reads the article in place, F asks for fewer from that source today, and − and + change the text size. Every screen change moves focus to its heading, and status changes are announced to screen readers.
The accessible names, landmarks, grouping, and titles are asserted in the DOM tests, but only listening finds awkward phrasing. A five-minute pass with VoiceOver on macOS (Cmd+F5 to toggle):
- Load the page. The title "Commonfeed" is announced, then the main heading.
- Open the rotor (Ctrl+Option+U) and check Landmarks: main, "Add real feeds", "Build a session", "Your record". Check Headings: one level-one heading and three level-two.
- Tab to the address field. It should be announced as "Feed or homepage address", not by its placeholder.
- Start the sample session. Expect the title "Item 1 of 5", then focus on the item heading.
- Tab through the item: "Open original, opens in a new tab", the "Was this worth your attention?" group with three toggle buttons that announce pressed or not pressed, then "Previous item" and "Next item" with no arrows read out.
- Press the right arrow key. The title changes to "Item 2 of 5" and the new heading is read.
- End the session. Expect "Session complete", a "Session totals" list of five, and the outcome question as a group of three buttons. Choosing one announces that the receipt was saved, or for the sample that none is kept.
- Real RSS and Atom source ingestion through the local server
- Three explicit intentions: Catch Up, Discover, Understand
- Five-, ten-, and fifteen-minute session contracts
- Transparent per-item ranking explanations
- A trust level per source, set by you and shown in each item's reasons
- Word-level focus matching that names the matched words and scores partial matches
- A text-size control, a per-source "fewer today" skip with undo, and single-key shortcuts for every session action
- Explicit feedback instead of passive dwell-time inference
- A real end state with no infinite scroll
- Private browser-only source choices and session receipts
- Separate planned and actual session time
- Active-time measurement pauses while the page is in the background
- Separate preview, original-open, worthwhile, and show-less signals
- Source-balanced selection prevents a prolific feed from dominating
- Explicit feedback influences later ranking
- Full feed text can be read inside Commonfeed when the publisher supplies it
- Feed redirects are followed, with every hop checked against the private-network rules
- Learned preferences decay over a 30-day half-life and are capped so they never override the chosen intention
- Only http and https links from feeds can become links in the session
- The same article syndicated by several sources appears once per session
- Feeds are parsed with a real XML parser (fast-xml-parser): RSS 2.0, RSS 1.0, and Atom including xhtml content. The only other dependencies are the two font packages
- The local server caches feeds for ten minutes, then revalidates with ETag and Last-Modified, and serves the last good copy if a source is down
- Pasting a homepage discovers its declared feeds, or probes the common feed paths when none are declared
- Sources can be imported from and exported to OPML, with categories preserved
- Each source shows its own item count, cache state, and any error inline
- Items previewed in one session are held back from later sessions for 60 days, unless nothing else remains
- Every item lists the exact signals behind its ranking: intention weight, trust, freshness, focus match, and your own feedback
- Session length is calibrated from your fully previewed sessions: finish early and the next session holds more, within bounds
- Past sessions and what your feedback has changed are visible on the start screen, with one-click clearing of either
- Reader mode: when a feed carries only a summary, one click fetches that article through the local server and shows its text in place; reads are counted separately from opens on the receipt
- Runs as a single command or a container image; dark theme; keyboard navigation with managed focus and live announcements
- “Useful later” is a toggle that keeps the item in a private saved list, exempt from seen tracking and separate from ranking feedback, that can be read as its own session
- A session that was only partly useful preselects Discover next time, and says so
- An interrupted session is resumed on reload for up to a day, with its time, feedback, and progress intact
- Time on each item is measured; timed in-place reads calibrate pace ahead of session totals, and the receipt shows where attention went
- The page makes no request to any host but the local server: fonts are self-hosted, article images come through a guarded local route, and every response carries a strict Content Security Policy, no-referrer, nosniff, and frame-denial headers
- Feed and article fetches connect to the exact address that passed the private-network check, so a DNS answer cannot change between the check and the connection
- Your data lives in a private file on your machine, mirrored from the browser on every change, with full backup, restore, and erase; storage writes never fail a click
- The local server refuses requests whose Host is not this machine, refuses cross-site requests to its API, and refuses cross-origin writes
- A sample session is offered before any source is added; it is labelled as fictional and leaves no receipt, signals, or seen marks
- The interface is tested in a DOM (happy-dom, the only dev dependency): discovery, status, sessions, focus, keyboard, saved items, resume, and reader mode
Accounts, posting, messaging, crawling, advertising, opaque personalization, newsletters, and closed-platform integrations remain outside this probe. Reader mode fetches a single original page only when you ask for it; nothing is prefetched or crawled.
MIT. See LICENSE. The DM Sans and Newsreader typefaces are not part of this package; they are installed from their own packages under the SIL Open Font License.
Releases are cut from the maintainer's machine with one command, which does what a CI release job would.
- Bump
versionin package.json, runnpm install --package-lock-only, add a## <version>section to CHANGELOG.md, and commit. npm run releaseis a rehearsal. It checks that the version is new on npm, untagged, described in the changelog, and cut from an up-to-date main; then it checks the last commit out on its own, runs the gate there, and does a publish dry run. Nothing leaves the machine.npm run release -- --publishdoes the same, then tags the commit, publishes to npm (you confirm with your security key in the browser), pushes main and the tag, and opens a GitHub release from the changelog section. If the publish does not complete, the tag is removed again.
The gate and the publish both run in a temporary checkout of the last commit, so what ships is exactly what is committed, never whatever is in the working directory.
.github/workflows holds a CI workflow and a tag-driven release workflow that publish through npm's trusted publishing and push a multi-arch image to ghcr.io. They are dormant because GitHub Actions is not available on this account, and need no changes when it is: enable Actions, then on npmjs.com add a Trusted Publisher for user zacharymplace, repository commonfeed, workflow release.yml.