Ctrl K
ComunidadGuías…

ARCHITECTURE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Lectio Architecture

Lectio is a self-hosted feed reader built around the `reader` Python library. The goal is a fast triage workflow with a clean multi-user architecture and VPS-friendly deployment.

## Contents

Design rationale, split by area. Each file is the *why* behind its
part of the system — what was tried, what broke, and what the current
shape is defending against.

- **[tenancy.md](docs/architecture/tenancy.md)** — Per-user isolation, the resolver seam, and what multi-user mode changes.
- **[feeds.md](docs/architecture/feeds.md)** — Subscribing, fetching, deduplicating, combining and removing feeds.
- **[views.md](docs/architecture/views.md)** — The post list, folders, sorting, layout and page weight.
- **[images.md](docs/architecture/images.md)** — Choosing, rejecting, sizing and serving the image for a post.
- **[reading.md](docs/architecture/reading.md)** — Read Mode, offline reading, and offline actions.
- **[saved.md](docs/architecture/saved.md)** — Read-it-later capture, keeping, and editing a post in place.
- **[integrations.md](docs/architecture/integrations.md)** — Per-user OAuth destinations, quotas, and the automation that drives them.
- **[apis.md](docs/architecture/apis.md)** — The sync APIs Lectio speaks to third-party clients.

## Layering

- UI/API layer: web routes, handlers, presentation state.
- Services layer: feed operations, tagging, filtering, refresh, readability.
- Storage layer: reader DB, app-data, settings.

The layers run in one process today, but the boundaries should stay clean.

### Route modules

Routes are split out of `main.py` into `routes/*.py` by URL prefix (`system`, `compat_{fever,greader,v1}`,
`tags`, `highlights`, `automation`, `admin`, `saved`, `settings`, `feeds`, `entries`, `home`), each a
plain `router = APIRouter()` included from `main.py`'s bottom-of-file block. Shared module-level state
(caches, locks, generation counters) lives in `state.py`; route modules import it back rather than
redefining it.

Gotchas that apply to any further extraction out of `main.py`: a module that imports a singleton
cache/lock must not redefine it, and every `invalidate_*` call site has to stay wired to the same
instance. `get_reader()`'s thread-local pooling and `lifespan` are startup-order-sensitive. A name
late-bound at the bottom of `main.py` (e.g. `_run_automation_after_refresh` from
`services.automation_rules`) needs the importing module's own import positioned after that block, or
import fails — and this cascades, since a route module that itself gets imported by another route
module needing the same treatment must move after it too. A scalar rebound via `global` needs a real
accessor function, not a bare imported name: `from state import _some_counter` freezes a snapshot at
import time and goes stale on every later write, the same failure mode a leftover `global` write has.
Before assuming a helper is single-route-only, grep `routes/*.py` and `scripts/*.py` too, not just
`main.py` and `tests/` — several "obviously local" helpers turned out to have script-only callers.

## Reader-first philosophy

`reader` is the primary storage/ops primitive. It already covers:
- feed retrieval and storage,
- read state,
- arbitrary tags and metadata,
- filtering and search,
- statistics,
- plugin support.

Prefer reader API and plugin behavior first. Add custom logic only when the existing reader model cannot express the behavior cleanly.

## Deployment path

Lectio is designed for VPS deployment behind a reverse proxy. Auth is always active; access requires a user account. See `.env.example` for deployment configuration.

## Extension strategy

Use plugin/adapter style for non-native behavior instead of hardwired branching. Prefer replaceable pieces and avoid duplicating `reader` capabilities in app code.

## Security direction

Keep the local-first path simple. Add auth only when exposing the app beyond trusted local use. The multi-user phase makes per-user identity, per-user API tokens, route-level authorization, and SSRF hardening mandatory — see [tenancy.md → Security posture](docs/architecture/tenancy.md#security-posture).

Every redirect built from request data goes through `services.redirects.local_url` (same-origin paths only), and a catch-all `except` that answers the client uses `services.errors.public_error` (fixed message, traceback logged) rather than echoing `str(exc)`. CodeQL treats both as sanitizers via the guard-aware queries in `.github/codeql/queries/`.