# 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.