Desktop RSS Reader

A local-first Electron RSS reader with offline caching and security hardening.

Signal Desk is a minimal viable product that fetches RSS 2.0 and Atom feeds, caches them offline, and provides a distraction-free reading pane. The app keeps networking, XML parsing, and file I/O in the main process behind a typed preload bridge so the renderer stays unprivileged.

Purpose

Signal Desk is a minimal, local-first RSS desktop app that prioritizes security, offline resilience, and clean architecture over feature breadth.

User problem

Many RSS readers rely on cloud sync or lack offline support. Signal Desk keeps your feeds and articles local, encrypted behind your system, without mandatory accounts or network dependency.

Target user

Developers, researchers, and knowledge workers who prefer desktop tools, value privacy, and want to read feeds without tracking or interruptions.

MVP scope

Fetch and parse RSS 2.0 and Atom feeds via HTTP validators (ETag/Last-Modified), cache articles locally, display them in a clean reader, mark as read, and export/import archives.

Current Status

Signal Desk v1.0.0 ships as a desktop-only MVP. The app is production-ready for core workflows but does not yet include folders, advanced search, AI organization, or cloud sync.

What works

  • Add and remove RSS feeds
  • Fetch RSS 2.0 and Atom feeds with HTTP caching validators
  • Display articles and mark them as read
  • Offline-first: articles persist even if refresh fails
  • Export and import feed archives as versioned JSON
  • Built-in text-to-speech for articles
  • Distraction-free reader with HTML stripped to safe plain text

Not yet in scope

  • Feed folders, organization, or tagging
  • Article search or advanced filtering
  • Cloud sync or cross-device profiles
  • AI-powered categorization or summaries
  • Rich HTML rendering or media enclosures
  • Notifications or app badges
  • Settings/profile surface

Architecture

Signal Desk follows Electron's three-process model with explicit boundaries. The renderer is fully unprivileged; the main process owns all I/O, networking, and XML parsing; and a typed preload bridge is the only bridge.

Renderer (React UI)
  └─> window.rss (typed preload bridge via contextBridge)
        └─> Main Process
              ├─ IPC handlers: rss:fetchFeeds, rss:addFeed, rss:toggleRead, etc.
              ├─ HTTP fetching with ETag/Last-Modified validators
              ├─ XML parsing (fast-xml-parser)
              ├─ Cache file I/O (<userData>/signal-desk/feeds.json)
              ├─ External URL handling (shell.openExternal)
              └─ Background auto-refresh (20-minute timer)

Main process

Owns all privileged work: networking via net.fetch, cache file I/O, RSS/Atom XML parsing, and opening external URLs. Registers guarded IPC handlers and runs a background refresh timer. Denies external navigation and new windows in the renderer.

Preload bridge

Exposes a single typed API, window.rss, via contextBridge (type RssApi in shared/types.ts). No raw IPC, Node, or filesystem access reaches the renderer. Each method is a guarded IPC call.

Renderer

An unprivileged React 18 app that calls only window.rss methods. Displays feed list, article list, and reader pane. Runs with contextIsolation: true, sandbox: true, and nodeIntegration: false.

FeedService

Domain logic: fetch with HTTP validators, parse RSS 2.0 and Atom, strip HTML to safe plain text, deduplicate articles with stable SHA-256 ids, cap cache at 300 articles per feed, preserve read state, and handle atomic writes and migrations.

Cache

A versioned JSON file at <userData>/signal-desk/feeds.json stores all feeds and articles. Versioning ensures safe schema migrations. Atomic writes prevent partial data loss on failure. Export archives use the same versioned format.

Security

Context isolation and sandbox prevent renderer escape. Feed HTML is stripped to plain text, never rendered with dangerouslySetInnerHTML. Feed URLs must be HTTP(S) without embedded credentials. HTTP connections require explicit user confirmation.

Core Features

Each feature is built around offline-first resilience and security. Articles are cached before display, feeds fail gracefully, and the reader respects user read state.

Feed fetching

HTTP GET with ETag and Last-Modified validators; servers return 304 Not Modified if content is unchanged. Avoids redundant downloads and respects server bandwidth.

RSS 2.0 and Atom parsing

fast-xml-parser parses both formats into a unified article schema. Malformed feeds are handled gracefully; valid articles are cached even if parsing fails partway.

Offline cache

Articles persist in a versioned JSON cache. A failed refresh never drops what you've already fetched. The cache survives app restarts and network outages.

Read state tracking

Mark articles as read per feed or globally. Read state is persisted and survives refresh. Unread count is derived from cache state for accuracy.

Archive export/import

Export all feeds and articles to a versioned JSON file via native save dialog. Import from a saved archive via native open dialog. Preserves feed metadata and read state.

Built-in text-to-speech

Web Speech API provides native TTS for the selected article. Controls for play, pause, and stop. No network call or cloud service required.

Tech Stack

Minimal dependencies chosen for stability, security, and native desktop support. No cloud services or network dependencies beyond public RSS feeds.

Desktop & runtime

  • Electron 43 — latest stable, native three-process model
  • Electron Forge — build, package, and distribution
  • Vite — fast dev server and build for renderer

Frontend

  • React 18 — UI framework
  • TypeScript 5.7 — strict typing for renderer and main process

Core logic

  • fast-xml-parser — RSS 2.0 and Atom parsing
  • Electron APIsnet.fetch, file I/O, shell.openExternal

Testing & quality

  • Vitest — unit tests for FeedService
  • ESLint + TypeScript plugin — linting
  • tsc --noEmit — type checking

Security Constraints

Signal Desk enforces hard boundaries between privileged and unprivileged code. These constraints ensure the app remains resilient to renderer escape and feed injection attacks.

Renderer isolation

  • Context isolation (contextIsolation: true)
  • Sandbox enabled (sandbox: true)
  • Node integration disabled (nodeIntegration: false)
  • No raw IPC, filesystem, or module access to renderer

Feed URL validation

  • Feed URLs must be public HTTP(S) without embedded credentials
  • Insecure HTTP (non-HTTPS) requires explicit user confirmation
  • No support for authenticated feeds or private HTTP

HTML and content handling

  • Feed HTML is always stripped to plain text
  • Never use dangerouslySetInnerHTML
  • External links open in system browser via shell.openExternal, not in-app

IPC strictness

  • Only explicit rss:* IPC methods are registered
  • Each handler validates sender and input before processing
  • No generic IPC event listeners or wildcard handlers

Cache versioning

  • Cache and export archive use version numbers
  • Schema changes require migration logic
  • Unknown data is preserved to support forward compatibility

Trusted sender check

  • All IPC handlers include a trusted-sender verification
  • Prevents injection via preload or worker threads

Verification

The project uses deterministic tests and CLI checks to prevent regressions. All verification commands must pass before shipping a release.

Commands

npm start             # Launch dev app
npm run lint          # ESLint on .ts, .tsx
npm run typecheck     # tsc --noEmit
npm test              # vitest run
npm run package       # Package without installers
npm run make          # Build installers

Test focus

  • FeedService: fetch, parse, cache, export/import
  • HTTP validator handling (ETag, Last-Modified, 304)
  • Article deduplication and read state persistence
  • Malformed XML and feed error resilience
  • Migration and cache version safety

Distribution: npm run make builds native installers via Electron Forge — Squirrel (Windows), ZIP (macOS), and .deb/.rpm (Linux). The app ships only as a desktop binary, never as a web deployment.

Planned Next Work

The roadmap expands Signal Desk from a basic feed list into a full reading workstation. Each item builds on the existing architecture and versioned-migration pattern.

  1. Feed and folder management. Edit feed URL and display title, duplicate detection, per-feed pause/refresh settings, reorder controls, folder creation/rename/delete, and feed-to-folder moves. Add OPML import/export alongside JSON.
  2. Article lifecycle and counts. Expose total, unread, saved, and archived counts per feed and global inbox. Add mark unread, star/save, archive/unarchive, bulk selection, and undo for destructive actions.
  3. Navigation and search. Inbox views for Everything, Unread, Saved, Starred, folders, and per-feed. Title/content/source search, sort order, date filters, virtualized pagination, and keyboard shortcuts.
  4. Rich content and media. Sanitized HTML rendering (not raw), images, video/audio enclosures, and a readable-text extraction pipeline. Keep plain-text fallback for accessibility.
  5. Hardened refresh and offline. Request timeouts, bounded response sizes, per-feed backoff, refresh concurrency limits, manual retry, and a diagnostics view. Preserve last articles on all errors.
  6. Configurable TTS. Language detection/override, voice selection, rate, pitch, volume, play/pause/resume/stop, chunking with progress, queue/auto-advance, and keyboard controls.
  7. Settings and profile. Local profile name/avatar, appearance/theme, density, default view/filter, refresh cadence, cache retention, TTS preferences, keyboard shortcuts, and privacy options.
  8. AI-assisted organization (opt-in). Main-process AI service with provider adapters for tagging, categorization, summaries, and topic extraction. Require explicit privacy consent, support batch processing, and allow local-model or rule-based fallback.