Skip to content

Latest commit

 

History

151 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mood Reader

A calm, self-hosted RSS reader for people who miss the old web.

There is no algorithm here. No ranking, no "For You", no engagement bait. Just the sites you chose to follow, in the order they published, in a quiet interface that gets out of your way. It runs as a single container next to a Postgres database, and it is multi-user from the ground up, so your family, your club, or your team can read on the same instance without stepping on each other.

Why you might like it

  • Reading first. Three list layouts (cards, list, magazine), comfortable and compact densities, and three article views: a clean readability extraction, the feed's own content, or the full web page. Long-form text is set in a proper serif at a readable measure.
  • Your keyboard works. j/k through articles, s to star, a to mark a view read, / to search, ? for the full shortcut overlay. The mouse is optional.
  • Fast, honest search. Postgres full-text search across everything you are subscribed to, with phrase and exclusion syntax, ranked sensibly. Save a search to the sidebar, and let rules mark read or star new articles for you.
  • It respects your attention. Unread-only mode, mark-all-read that does what it says with Undo, per-feed controls (custom titles, Skim / Must read attention levels, hide from All Items), and folders and subfolders that drag and drop.
  • It looks after itself. Feeds are discovered from a plain site URL, favicons fetched, HTML sanitized server-side before it ever reaches your browser, and articles deduplicated globally no matter how many users subscribe.
  • It works offline. Installable PWA with cached articles, so the train tunnel does not end your morning read.
  • It is part of the open web. Share what you read on a public page that is itself a feed, publish your blogroll, get posts in seconds from feeds with a WebSub hub, and follow Mastodon and Bluesky people like any blog. A star keeps a readable copy, so a dead link does not take the article with it.
  • It is yours. OPML import and export (leave whenever you like), eight named themes from paper-warm to void-black, sessions in your own database, and not a single external service in the serving path.
  • Built to be read by screen readers too. Live-region announcements, focus-managed dialogs, skip links, labeled controls.

The first account to register becomes the admin; after that, open registration, invites, or closed, your call.

Stack

Layer Choice
Language TypeScript everywhere
Web React 19 + Vite, React Router, TanStack Query
Styling Tailwind CSS v4 + shadcn/ui (Radix)
API Fastify 5
Worker Node process polling feeds on an interval
Database PostgreSQL via Drizzle ORM
Auth Self-rolled session cookies (argon2id hashing)
Monorepo pnpm workspaces + Turborepo

Layout

apps/
  web/      React SPA (Vite)
  api/      Fastify API + feed-polling worker + Drizzle schema
packages/
  shared/   Zod schemas + types shared by web and api
docker/     Dockerfile + compose files
.github/    CI (lint/typecheck/test/build) + GHCR image publish

The API serves the built SPA from the same origin in production, so the whole app is one container image (plus Postgres).

Getting started (development)

Prerequisites: Node 22+, pnpm, Docker.

cp .env.example .env          # then edit SESSION_SECRET at minimum
pnpm install

# Start Postgres only; run the app on the host for hot reload.
pnpm docker:dev
pnpm db:migrate               # apply the migrations committed in apps/api/drizzle

pnpm dev                      # web on :5173, api on :3000 (proxied)

Register the first account through the UI. The first user to register becomes the admin.

To run the feed worker locally:

pnpm --filter @rss/api dev:worker

Production (single stack)

cp .env.example .env          # set a strong SESSION_SECRET and DB password
docker compose -f docker/docker-compose.yml up -d --build

This starts Postgres, runs migrations once, then launches the API (serving the SPA on :3000) and the feed worker.

Useful scripts

Command What it does
pnpm dev Run web + api in watch mode
pnpm build Build all packages
pnpm lint ESLint across the workspace
pnpm typecheck tsc --noEmit across the workspace
pnpm test Run tests (Vitest)
pnpm db:generate Generate Drizzle migrations from the schema
pnpm db:migrate Apply migrations
pnpm db:studio Open Drizzle Studio

Data model

Feeds and articles are stored once, globally, and deduplicated by URL / guid. Per-user data lives in subscriptions (which feeds a user follows) and article_states (read / starred). This keeps storage flat as users grow and lets the worker fetch each feed a single time regardless of how many people subscribe.

What it does today

  • Feed discovery from a site URL + favicon fetching
  • OPML import / export
  • Reading views: cards, list, magazine (+ comfortable/compact density)
  • Article views: Feed (the feed's own content), Extracted (readability), and Web (the full page)
  • Full-text search (Postgres tsvector)
  • Keyboard-driven navigation (j/k, mark read, star)
  • Mark-all-read, filters (unread / starred / by folder)
  • Keyset pagination + infinite scroll
  • HTML sanitization of article content
  • PWA / offline support
  • Per-user settings + themes
  • Admin, invites, and multi-user management

The open-web era

The phase that leans into what made the old web good. Each item has its spec in docs/design-specs.

  • Sharing and shared items (SPEC-019): a proper share button, Google-Reader-style shared items with notes, an opt-in public linkblog at /u/you that is itself an Atom + JSON feed others can subscribe to, and a Community view of what others on your instance share.
  • Public blogrolls (SPEC-020): a "who I read" page with an importable OPML twin.
  • Realtime delivery via WebSub (SPEC-021): new posts arrive in seconds when a feed offers a hub; polite polling otherwise.
  • Attention levels (SPEC-022): mark a feed Skim (no unread guilt, ever) or Must read (never miss a post).
  • Follow the new social web too (SPEC-023): paste a Mastodon, Bluesky or Medium profile, or a @user@instance handle, and it just subscribes; their pages were feeds all along.
  • Link-rot armor (SPEC-024): starring keeps a readable copy of the page, every article has a Wayback Machine link for pages that already died, and admins can set how long to keep old articles (starred and shared ones are always kept).
  • Saved searches and rules (SPEC-025): save a search, scope and all, to the sidebar, and set rules that mark read or star new articles as they arrive ("title contains sponsored: mark it read"), with a button to run a rule over older articles too.
  • IndieWeb identity (SPEC-026): add your website, a photo and your other profiles, and your public pages carry a proper h-card with rel="me" links (so Mastodon can show them as verified), shares marked up as bookmarks, and blogroll discovery.

What's coming

Every spec in docs/design-specs is built. New work starts as a new spec there.

About

A calm, self-hosted, multi-user RSS reader for people who miss the old web.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages