Work in progress. This README covers features and design intent. Setup documentation is forthcoming.
Lectio is a self-hosted feed reader focused on fast reading triage, rich content handling, and automation. It runs well on a personal VPS with full multi-user support, and is built to keep feed reading fast, keyboard-friendly, and workflow-oriented.
A self-hosted RSS reader with a triage-first interface that adapts from a three-pane desktop layout to narrower tablet and phone workflows. Built on Python + FastAPI + the reader library, with a plain-HTML/JS frontend — no build step, no bundler, no framework.
The design priority is speed of triage: quickly marking things read, surfacing what matters, and staying out of the way.
| Dark mode | Light mode |
|---|---|
![]() |
![]() |
More shots (settings, automation, feed properties, tags, history, admin) are in the Screenshots wiki page.
Full detail lives in the wiki — Features and Multi-user & APIs. The short version:
- Fast triage — three-pane reader, keyboard nav, context menus, bulk mark-as-read, manual tags, read history, search, and a Readability/web-view proxy.
- Rich content — embeds that actually render (curated trusted-host allowlist),
inline podcast players (incl. audio borrowed from a separate host feed), file
attachments, recovered YouTube embeds, and bare-text feed cleanup. When an older
article lost its player (the feed stripped the
<iframe>before Lectio kept them), the missing YouTube/Bandcamp/SoundCloud embed is recovered from the source page and re-attached. Bandcamp single-track players (domain-locked to the original publisher, so they'd otherwise show "not available") fall back to the album player so they actually stream. Titles that arrive HTML-encoded (or double-encoded, as Tumblr does —Magus’ Castle) are decoded so they read correctly instead of showing the raw entity. A bare YouTube or Bandcamp album/track link sitting alone in its own paragraph (common when a feed strips the oEmbed iframe) is turned into an inline player. (Bandcamp resolves the numeric embed ID from the album page on first open and caches it; the embed appears on the next open when the page isn't yet cached.) Reader view re-injects allowlisted players (YouTube/Spotify/Bandcamp) that the readability extractor would otherwise strip — audio players (Bandcamp/SoundCloud/Spotify) keep their proper fixed height instead of a 16:9 video box — and de-duplicates a repeated lead image. YouTube embeds default to the privacy-enhanced host; a per-user Integrations setting switches them to the standard host so Share / Watch Later work, and connecting a YouTube account (per-user OAuth) adds an Add to playlist control beneath each video embed (lists your playlists, creates new ones). A global Integrations toggle auto-hides Shorts across all YouTube feeds, and a quota meter estimates your daily YouTube API usage against the cap. - Lead images — per-feed extraction strategies with side-by-side comparison, smart crop/fit tuning, caption sourcing, junk-image rejection, inline-SVG art, and full-resolution webcomic panels (ComicControl thumb→full promotion). List thumbnails fall back to a direct browser load when the server-side image proxy is refused (some hosts IP-block the server but serve your own IP fine).
- Automation — highlight, mark-as-read, deduplicate, email-article,
outbound-webhook (with an optional batch mode that groups all matches from
one refresh run into a single
{entries:[...]}request instead of one call per entry), save-to-Instapaper, add-to-YouTube-playlist, and add-to-Quire rules (the YouTube rule auto-adds new videos — including those embedded in any feed's article — to a chosen playlist, with include-Shorts, mark-read, and min/max-duration options; quota-capped, no double-adds); scope a rule to all feeds, a folder, a single feed, or a multi-selected set of feeds (deduplicate can run across a selected set of feeds, not just a whole folder), with a Duplicate button to clone one quickly; all fire at refresh time with a manual "Run Now". Starring an article can also auto-send it to Instapaper, a YouTube playlist, email, and/or Quire (Integrations → On Star). - Save to Pinterest — connect a Pinterest account (per-user OAuth) and a
Pin button appears on each article, saving its lead image (linked back to
the source) to a board you pick. Needs
PINTEREST_OAUTH_CLIENT_ID/SECRET; entries without an image can't be pinned. - Add to Quire — connect a Quire account (per-user OAuth)
and pick a destination project; an Add to Quire button then appears on each
article and creates a task (titled from the entry, with the link in the
description). Also available via On Star and Automation rules. Quire's
per-organization minute/hour rate limits are tracked with a usage meter in
Settings, and automation runs are capped and back off on a 429. Register an app
at quire.io/apps/dev with redirect URI
https://<your-host>/quire/callback; creds are per-user (orQUIRE_CLIENT_ID/SECRETas instance-wide fallback credentials). - Feed management — OPML, RSS/Atom auto-discovery, Page Feeds, YouTube & DeviantArt sync, per-folder cadence, feed compare, fetch-history & automations tabs, and duplicate-feed scanning.
- Reliability — conditional GET, per-feed/domain backoff, GUID-churn suppression, WebSub real-time push, WAL-mode SQLite, and browser-identity fetch fallback for feeds whose servers refuse the default client.
- Multi-user — isolated per-user databases with shared content caches; GReader, Fever, and Miniflux v1 API compatibility; Instapaper & email integrations.
- Data portability — Takeout-style ZIP export/import and online-safe backups.
| Layer | What it does |
|---|---|
main.py |
FastAPI routes, Jinja2 templates, all request handling |
services/ |
Feed refresh, lead images, email, starred archive, YouTube, reader API wrapper |
reader library |
Feed fetching, parsing, storage, ETag/conditional requests |
lectio.db |
reader's SQLite feed+entry store |
lectio_meta.sqlite3 |
App state: prefs, automation rules, lead images, read history, failure tracking |
lectio_meta.sqlite |
Starred/saved entry archive |
- Backend: Python 3.14, FastAPI, uvicorn
- Feed library: reader (handles HTTP, parsing, ETags, scheduling)
- Frontend: Vanilla JS, Jinja2 templates, no build step
- Database: SQLite (WAL mode) × 3
- Deployment: Docker + docker-compose, Traefik reverse proxy
- Tests — pytest suite (unit, services, integration, scripts) under
tests/. Run withuv run pytest. - CI — GitHub Actions runs the suite on Python 3.14 for every pull request and push to
main(.github/workflows/ci.yml). Dependencies install from the lockeduv.lock(uv sync --frozen), and the run treats anyDeprecationWarningas an error so they surface immediately rather than accumulating. - Dependency audit —
uv audit(OSV-backed) scans the locked dependencies for known vulnerabilities and deprecated packages. Run it locally withmake audit; CI runs the same scan. It's a uv preview feature, so it's kept separate frommake testlocally and the CI step is informational (non-blocking) for now.
Active personal use. Not yet documented for general deployment. The codebase moves fast — APIs, DB schema, and config format may change without notice.
Issues and PRs welcome, but this is primarily a personal project.

