docs: record the domain model and the Postgres/OAuth ADRs
Written while scoping #18. CONTEXT.md pins the ubiquitous language (Series, Reader, Bookmark, Progress, Latest Chapter, Poll) that the schema split and the ordering rule are argued in; the four ADRs record the decisions that follow from it, starting with Postgres over SQLite. Refs #18
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Series is a shared entity, and only the Poll may update it
|
||||
|
||||
Status: accepted
|
||||
|
||||
Facts about a Series that are true regardless of who is reading — title, cover, canonical
|
||||
URL, Latest Chapter — moved off the Bookmark onto a shared `series` row keyed
|
||||
`(site, series_id)`. A Bookmark now holds only what differs between Readers: Progress,
|
||||
Favourite, Lifecycle bucket. Fifty Readers tracking one Series produce fifty Bookmarks
|
||||
and one Series, so the Series is polled once rather than fifty times.
|
||||
|
||||
## Why
|
||||
|
||||
The poller checks at most 84 series/hour (batch 14 per 10-minute tick). With ~50 Readers
|
||||
holding ~30 Series each, polling per Bookmark means a 1,500-item sweep — roughly 18 hours
|
||||
against a configured 1-hour cooldown, quietly breaking the New Chapter signal that is the
|
||||
product's reason to exist. Deduplicating to distinct Series cuts the sweep several-fold,
|
||||
and because the Series row now knows how many Readers hold it, the poll queue is ordered
|
||||
`reader_count DESC, latest_checked_at ASC` — popular Series stay fresh and the long tail
|
||||
absorbs the shortfall. That ordering is only expressible because the split happened.
|
||||
|
||||
Raising throughput instead was rejected: sweeping 400 Series hourly needs the stagger
|
||||
cut from 20s to ~9s, doubling request rate against sites that already bot-score the
|
||||
single VPS IP.
|
||||
|
||||
## Only the Poll writes Series fields
|
||||
|
||||
A client may supply `title`, `cover` and `series_url` only when creating a Series nobody
|
||||
has bookmarked yet. After that, client-supplied values are ignored; only the backend's
|
||||
own fetch updates them.
|
||||
|
||||
This is a security boundary, not tidiness. Those values are scraped from third-party
|
||||
pages, which `AGENTS.md` requires be treated as attacker-controlled. Before the split, a
|
||||
hostile or compromised site could corrupt exactly one Reader's row. After it, the same
|
||||
write lands on a row every Reader sees — one Reader's browser becomes a write path into
|
||||
everyone else's UI, and a cover URL can point anywhere. The backend's own fetch is the
|
||||
higher-trust source: its network, its parser, no third-party JavaScript in the path.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `Store.Upsert` decomposes one incoming flat body across two tables and enforces the
|
||||
ownership rule at that seam.
|
||||
- The `updated_at` ordering rule stays on the Bookmark, where Progress lives. Unchanged.
|
||||
- Per-Reader title overrides are deliberately not supported; they would reintroduce the
|
||||
duplication this removes.
|
||||
Reference in New Issue
Block a user