Every doc statement that explained a Cloudflare challenge as a "score" was wrong. Researched against Cloudflare's own docs on 2026-08-12 (docs/research/cloudflare-bot-scoring-and-poll-cadence.md, 22 primary pages plus RFC 9309): the 1-99 bot score is Enterprise Bot Management only, free-plan zones get Bot Fight Mode signature matching and no score at all, and no per-IP request rate is documented as an input to challenge issuance. cf_clearance also expires in 30 minutes, so every cadence at or above 1h re-solves the challenge regardless. Docs only - no behaviour change. The 6h browser cooldown stays; its justification is now cost (a serialized single-tab solve costs seconds, a plain read costs one request), not a risk reduction nothing documents. - AGENTS.md: the block is per-zone configuration plus request fingerprint, not IP reputation; comix.to turning its gate on 2026-08-12 is the example. Residential egress avoids the cloud-hosting-IP signature rather than earning a better score. The UTC measurement stands but its mechanism is marked undocumented. - backend/AGENTS.md: states why the browser cooldown is longer. - ADR-0003, ADR-0006: dated corrections rather than rewrites. Both decisions stand on their other arguments (sweep depth, VPS memory). - DEPLOY.md: a red smoke run means the Site's settings or this Chrome's fingerprint moved, not that "Cloudflare's scoring" did.
2.8 KiB
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 already fronted by Cloudflare from the single VPS IP.
Corrected 2026-08-12: the original wording said those sites "bot-score" the VPS IP.
They do not — the 1-99 bot score is Enterprise Bot Management only, and no per-IP
request rate is documented as an input to challenge issuance
(docs/research/cloudflare-bot-scoring-and-poll-cadence.md). The decision stands on
its first argument, sweep depth versus the 1-hour cooldown; the rate-limit fear was
never evidenced.
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.Upsertdecomposes one incoming flat body across two tables and enforces the ownership rule at that seam.- The
updated_atordering rule stays on the Bookmark, where Progress lives. Unchanged. - Per-Reader title overrides are deliberately not supported; they would reintroduce the duplication this removes.