@isomoes/iread
v0.2.3
Published
Local, single-user RSS/Atom reader with a keyboard-first three-pane web UI, inspired by newsboat.
Maintainers
Readme
iread
A concise, local, single-user RSS/Atom reader inspired by newsboat. Keyboard-first three-pane UI, TypeScript end to end. You add feeds by URL, refresh them on demand, and read server-sanitized article bodies in a calm, terminal-quiet reading surface with light, dark, and system themes.
There is no auth, no multi-user, no cloud sync, and no background scheduler. Refresh is always user-initiated. All data lives in a single local SQLite file — by default ~/.config/iread/iread.db — which is the trust boundary.
📺 Watch the v0.2.2 intro video on Bilibili.
Quick start
npx @isomoes/ireadThen open http://localhost:8787. Requires Node.js 24 or newer (the server uses the built-in node:sqlite module). Data is stored in ~/.config/iread/iread.db ($XDG_CONFIG_HOME is honored).
Usage: iread [options]
Options:
-p, --port <port> Port to listen on (default: $PORT or 8787)
--db <path> SQLite database file
(default: $DB_PATH or ~/.config/iread/iread.db)
--opml <path> OPML file auto-saved on every subscription change
(default: $OPML_PATH or feeds.opml next to the database;
pass "" to disable)
-v, --version Print the version and exit
-h, --help Show this help and exitRun with Docker
Prebuilt multi-arch images (amd64 + arm64) are published to the GitHub Container Registry, so no Node.js on the host is required:
docker run -d --name iread -p 8787:8787 -v iread-data:/data ghcr.io/isomoes/iread:latestThen open http://localhost:8787. The SQLite database and the auto-saved feeds.opml mirror live under /data in the container — here the named iread-data volume — so your subscriptions survive container upgrades.
A docker-compose.yml is included for the same thing:
docker compose up -dBuild the image from the checkout instead of pulling it with docker compose up -d --build, or docker build -t iread ..
PORT (default 8787), DB_PATH (default /data/iread.db), and OPML_PATH (default /data/feeds.opml) are configurable via -e/environment:. To store data in a host directory instead of a named volume, bind-mount it and make it writable by the image's unprivileged node user (uid 1000), e.g. mkdir iread-data && sudo chown 1000:1000 iread-data then -v "$PWD/iread-data:/data".
Features
- Add a feed by URL (title resolved from feed metadata), delete a feed.
- Refresh one feed (
r) or refresh all feeds (R). - Sidebar feed list with per-feed unread counts plus global smart-view totals.
- Smart views: All, Unread, Starred, applied across the article list, with per-feed selection.
- Article list with read/unread indicator, source feed, relative time, and star indicator.
- Reader pane with title, author, date, server-sanitized HTML body, and open-original link.
- Mark read/unread, auto-mark-read on open (and, on desktop, when you move on from a viewed item), mark-all-read for the current feed or view.
- Star and unstar.
- Live case-insensitive search over title plus summary in the current view.
- Full keyboard navigation as the primary interaction model.
- OPML import (bulk add) and OPML export, plus an always-current
feeds.opmlauto-saved next to the database on every change for quick sharing and backup. The auto-saved file is a one-way snapshot of the database (overwritten on every change and at startup), so use it for backup and sharing — to bring feeds in, use OPML import. - Light, dark, and system theme, persisted to localStorage.
Usage
- Start the app (dev or production) and open it in your browser.
- Add a feed by pasting its RSS or Atom URL into the add-feed form in the sidebar, or import an OPML file from the OPML menu. A
config/sample-feeds.opmlfile is included for a quick start, andisomoes/arch-config/iread/feeds.opmlis a real-world example (the feed set isomoes actually reads) you can import directly. - Select a feed or a smart view (All, Unread, Starred) in the sidebar.
- Navigate the article list with
jandk, open an article withEnter, and read it in the right pane. - Press
rto refresh the current feed orRto refresh all feeds. Refresh fetches remote feeds; client polling only refreshes local data and never re-fetches remotely. - Star with
s, toggle read withm, mark a whole scope read withA, and search with/. - Press
?at any time to see the full keyboard map. Presstto cycle the theme.
Keyboard shortcuts
Keys are case-sensitive (Shift matters). Bindings fire only when you are not typing in an input and no Ctrl, Meta, or Alt modifier is held.
| Key | Action | Effect |
| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| j / Down | Down in focused pane | Pane-contextual: move the article selection (list), the feed/view selection (sidebar), or the reader scroll down one. Stops at the end (no wrap). |
| k / Up | Up in focused pane | As j, upward. Stops at the start. |
| n | Next unread | Jump to the next unread item below; wrap to the first unread from the top if none below. |
| g | Top | Pane-contextual: in the list, select the first item; in the sidebar, jump to the first feed/view; in the reader, scroll the article to the top. |
| G | Bottom | Pane-contextual: in the list, select the last item; in the sidebar, jump to the last feed/view; in the reader, scroll the article to the bottom. |
| Enter / o | Open / focus reader | Render the selected item, mark it read, and move focus into the reader. |
| J / ] | Next feed/view | Move sidebar selection down and load its items, selecting the first one. |
| K / [ | Previous feed/view | Move sidebar selection up and load its items. |
| m | Toggle read/unread | Flip the read state of the selected item; counts update. |
| s | Toggle star | Flip the starred state; in the Starred view an unstarred item leaves the list. |
| A | Mark feed/view read | Mark the current scope read and offer an Undo toast. |
| r | Refresh current feed | Refresh the selected feed, or the feed of the selected article in a smart view. |
| R | Refresh all feeds | Refresh every feed and update counts on completion. |
| v | Open original | Open the article link in a new tab. |
| # then N | Open link by number | Links in the article body are numbered inline [N]; press #, type the number, and it opens in a new tab — instantly once the number is unambiguous, otherwise Enter confirms; Esc cancels. |
| / | Focus search | Focus and select the search input. |
| Esc | Contextual dismiss | Close help, clear search, or return focus from the reader to the list. |
| ? | Help overlay | Toggle the keybinding overlay. |
| t | Toggle theme | Cycle light, dark, and system theme; persisted. |
Project layout
src/shared/shared, type-only DTOs used by both server and web.src/server/Hono API, SQLite access, feed fetch/parse/sanitize, OPML, SSRF guard.src/web/React app: three-pane layout, hooks, components, styles.data/development SQLite database (gitignored); production data lives in~/.config/iread/.
Requirements (development)
- Node.js 24 or newer (the server uses the built-in
node:sqlitemodule). - pnpm.
Install
pnpm installDevelop
pnpm devThis runs two processes with concurrently:
- The API server on port 8787 (
tsx watch src/server/index.ts). - The Vite dev server on port 5173 with HMR.
Vite proxies /api to http://localhost:8787, so the browser only ever talks to 5173 and there are no CORS concerns.
In development (NODE_ENV is not production) the database defaults to data/iread.db inside the repo (gitignored), so dev experiments never touch your real ~/.config/iread data.
Open http://localhost:5173
Build and start (production)
pnpm build
pnpm startpnpm buildcompiles the server todist/server/(with shared types indist/shared/) and bundles the web app todist/web/.pnpm startruns a single Hono process that serves the API and the static web bundle with SPA fallback.
Open http://localhost:8787
PORT (default 8787), DB_PATH (default ~/.config/iread/iread.db), and OPML_PATH (default feeds.opml next to the database; set empty to disable the auto-saved mirror) are read from the environment. See config/.env.example.
