A modern, self-hosted RSS feed reader inspired by Google Reader. Your friendly librarian for organizing and curating the web's knowledge. Built with Nuxt 3, Vue 3, and Cloudflare (Workers + D1 + R2).
- ✅ Subscribe to RSS/Atom feeds
- ✅ Clean, distraction-free reading experience
- ✅ Mark articles as read/unread or bulk mark all read
- ✅ Manual feed syncing with per-user rate limiting
- ✅ Filter by feed, tag, or read status
- ✅ Cloudflare D1 for relational data
- ✅ Cloudflare R2 for article content + saved notes storage
- ✅ HTML sanitization for safe article rendering
- ✅ Fast, lightweight Nuxt 3 frontend
- ✅ Discover — new feeds recommended by the blogs you already read (blogroll/OPML crawl of your subscriptions' sites, ranked by recommender count)
- Node.js 22.x or later
- npm
- Cloudflare account with Workers, D1, and R2
-
Clone the repository:
git clone https://github.com/phareim/reader.git cd reader -
Install dependencies:
npm install
-
Configure your environment:
cp .env.example .env.local # Edit .env.local with any optional API keys -
Create a D1 database and apply the schema:
wrangler d1 execute <db-name> --file=database/d1-schema.sql
-
Start the development server:
npm run dev
-
Open http://localhost:3000 in your browser.
- Hacker News:
https://hnrss.org/frontpage - TechCrunch:
https://techcrunch.com/feed/ - The Verge:
https://www.theverge.com/rss/index.xml - Daring Fireball:
https://daringfireball.net/feeds/main
Nuxt 3 SPA ↔ Nitro server routes ↔ Cloudflare Workers/D1/R2
See database/d1-schema.sql for the full schema. Article content is stored in R2.
GET /api/feeds— List feeds with unread countsPOST /api/feeds— Subscribe to a new feedDELETE /api/feeds/:id— Remove a feed (cascades to articles)POST /api/feeds/:id/refresh— Refresh a specific feedGET /api/articles— List articles with filteringPATCH /api/articles/:id/read— Toggle read statusPOST /api/articles/mark-all-read— Bulk mark articles as readPOST /api/sync— Sync all active feeds for the current user
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Nuxt 3 (Vue 3) | UI & routing |
| Styling | Tailwind CSS | Utility-first styling |
| State | Nuxt composables | Client-side state |
| Backend | Nitro server routes | REST-style API |
| Database | Cloudflare D1 | Relational storage |
| Storage | Cloudflare R2 | Article content blobs |
| Auth | Custom (PBKDF2 + cookies) | Email/password login |
reader/
database/
d1-schema.sql
server/
api/
feeds/
articles/
sync/
utils/
composables/
pages/
types/
.env.example
Optional environment variables in .env.local:
FETCH_TIMEOUT=30000
MAX_ARTICLES_PER_FEED=200
ANTHROPIC_API_KEY="your-key" # For AI features
OPENAI_API_KEY="your-key" # For AI featuresNo auth-specific env vars needed — authentication uses the D1 database directly.
Available npm scripts:
npm run dev # Start development server
npm run build # Production build
npm run preview # Preview production build locally
npm run mcp # MCP server for Claude DesktopTo reset D1 data during development, drop and re-run the SQL from database/.
- ✅ Add/delete feeds
- ✅ Fetch and display articles
- ✅ Mark articles as read/unread
- ✅ Manual feed syncing
- ✅ Basic UI with Tailwind CSS
- ✅ Cloudflare D1 database + R2 storage
- ✅ HTML sanitization
- ✅ Email/password login
- ✅ Keyboard shortcuts (j/k navigation)
- Three-pane Google Reader-style layout
- ✅ Tags for feeds
- Full-text search
- OPML import/export
- ✅ Dark mode
- Automated background sync
- Mobile-responsive design improvements
- Require authentication everywhere sensitive data is returned (lock down
/api/articles/[id].get.tsand/api/feeds/[id].get.tsinstead of letting anonymous callers enumerate other users’ content). - Make the feed/article pages truly public or truly private—either stop hitting auth-only APIs when logged out or update those APIs to support the anonymous “share” views advertised by the UI.
- Ensure route protection actually runs (use a global middleware or add
middleware: 'auth'to protected pages; the currentdefinePageMeta({ auth: true })flag is inert). - Use D1
INSERT OR IGNORE(or similar) when backfilling articles so duplicate GUIDs do not abort the entire batch during feed creation/sync. - Skip fetching adjacent articles (or expose a readonly public endpoint) when the user is logged out so shared article pages don’t spam 401 errors and lose swipe navigation.
- Expand the Jest suite to cover key composables/server handlers listed in
collectCoverageFrom.
Suggestions and bug reports are welcome. Find me at X on BlueSky.
Inspired by the legendary Google Reader (RIP 2013).